diff --git a/submodules/HockeySDK-iOS/.gitattributes b/submodules/HockeySDK-iOS/.gitattributes new file mode 100644 index 0000000000..854d90babf --- /dev/null +++ b/submodules/HockeySDK-iOS/.gitattributes @@ -0,0 +1 @@ +*.pbxproj merge=union \ No newline at end of file diff --git a/submodules/HockeySDK-iOS/.gitignore b/submodules/HockeySDK-iOS/.gitignore new file mode 100644 index 0000000000..db0ab09450 --- /dev/null +++ b/submodules/HockeySDK-iOS/.gitignore @@ -0,0 +1,25 @@ +fastlane/README.md +fastlane/report.xml +fastlane/test_output/* +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.xcscmblueprint +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +.DS_Store +*.dSYM +*.dSYM.zip +*.ipa +*/xcuserdata/* +HockeySDK-iOS.xcodeproj/* diff --git a/submodules/HockeySDK-iOS/BUCK b/submodules/HockeySDK-iOS/BUCK new file mode 100644 index 0000000000..1921be1a0d --- /dev/null +++ b/submodules/HockeySDK-iOS/BUCK @@ -0,0 +1,93 @@ +load('//tools:buck_utils.bzl', 'config_with_updated_linker_flags', 'combined_config', 'configs_with_config') +load('//tools:buck_defs.bzl', 'SHARED_CONFIGS', 'LIB_SPECIFIC_CONFIG') + +genrule( + name = 'CrashReporter_lib_file', + srcs = [ + 'Vendor/libCrashReporter.a', + ], + bash = 'mkdir -p $OUT; cp $SRCS $OUT/', + out = 'CrashReporter_lib_file', + visibility = [ + '//submodules/HockeySDK-iOS:...', + ] +) + +apple_library( + name = 'CrashReporter', + visibility = [ + '//submodules/HockeySDK-iOS:...' + ], + header_namespace = 'CrashReporter', + exported_headers = glob([ + 'Vendor/include/**/*.h', + ]), + exported_linker_flags = [ + '-lCrashReporter', + '-L$(location :CrashReporter_lib_file)', + ], +) + +'''apple_library( + name = 'CrashReporter', + framework = 'Vendor/CrashReporter.framework', + preferred_linkage = 'static', + visibility = ['//submodules/HockeySDK-iOS:...'] +)''' + +apple_library( + name = 'HockeySDK', + srcs = glob([ + 'Classes/*.m', + 'Classes/*.mm', + ]), + headers = glob([ + 'Classes/*.h', + ]), + header_namespace = 'HockeySDK', + exported_headers = [ + 'Classes/HockeySDKFeatureConfig.h', + 'Classes/HockeySDKEnums.h', + 'Classes/HockeySDKNullability.h', + 'Classes/BITAlertAction.h', + + 'Classes/BITHockeyManager.h', + + 'Classes/BITHockeyAttachment.h', + + 'Classes/BITHockeyBaseManager.h', + 'Classes/BITCrashManager.h', + 'Classes/BITCrashAttachment.h', + 'Classes/BITCrashManagerDelegate.h', + 'Classes/BITCrashDetails.h', + 'Classes/BITCrashMetaData.h', + + 'Classes/BITUpdateManager.h', + 'Classes/BITUpdateManagerDelegate.h', + 'Classes/BITUpdateViewController.h', + 'Classes/BITHockeyBaseViewController.h', + 'Classes/BITHockeyManagerDelegate.h', + ], + modular = True, + configs = configs_with_config(combined_config([SHARED_CONFIGS, LIB_SPECIFIC_CONFIG])), + compiler_flags = [ + '-w', + '-DBITHOCKEY_VERSION=@\"5.1.2\"', + '-DBITHOCKEY_C_VERSION="5.1.2"', + '-DBITHOCKEY_C_BUILD="108"', + '-DHOCKEYSDK_FEATURE_CRASH_REPORTER=1', + '-DHOCKEYSDK_FEATURE_UPDATES=1', + '-DHOCKEYSDK_FEATURE_FEEDBACK=0', + '-DHOCKEYSDK_FEATURE_AUTHENTICATOR=0', + '-DHOCKEYSDK_FEATURE_METRICS=0', + ], + preprocessor_flags = ['-fobjc-arc'], + visibility = ['PUBLIC'], + deps = [ + ':CrashReporter', + ], + frameworks = [ + '$SDKROOT/System/Library/Frameworks/Foundation.framework', + '$SDKROOT/System/Library/Frameworks/UIKit.framework', + ], +) diff --git a/submodules/HockeySDK-iOS/Classes/BITActivityIndicatorButton.h b/submodules/HockeySDK-iOS/Classes/BITActivityIndicatorButton.h new file mode 100644 index 0000000000..2c633222c9 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITActivityIndicatorButton.h @@ -0,0 +1,33 @@ +/* + * 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 + +@interface BITActivityIndicatorButton : UIButton + +- (void)setShowsActivityIndicator:(BOOL)showsIndicator; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITActivityIndicatorButton.m b/submodules/HockeySDK-iOS/Classes/BITActivityIndicatorButton.m new file mode 100644 index 0000000000..6a731bb434 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITActivityIndicatorButton.m @@ -0,0 +1,82 @@ +/* + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "BITActivityIndicatorButton.h" + +@interface BITActivityIndicatorButton() + +@property (nonatomic, strong) UIActivityIndicatorView *indicator; +@property (nonatomic) BOOL indicatorVisible; + +@end + +@implementation BITActivityIndicatorButton + +- (void)setShowsActivityIndicator:(BOOL)showsIndicator { + if (self.indicatorVisible == showsIndicator){ + return; + } + + if (!self.indicator){ + self.indicator = [[UIActivityIndicatorView alloc] initWithFrame:self.bounds]; + [self addSubview:self.indicator]; + [self.indicator setColor:[UIColor blackColor]]; + } + + self.indicatorVisible = showsIndicator; + + if (showsIndicator){ + [self.indicator startAnimating]; + self.indicator.alpha = 1; + self.layer.borderWidth = 1; + self.layer.borderColor = [UIColor lightGrayColor].CGColor; + self.layer.cornerRadius = 5; + self.imageView.image = nil; + } else { + [self.indicator stopAnimating]; + self.layer.cornerRadius = 0; + self.indicator.alpha = 0; + self.layer.borderWidth = 0; + + } + +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + [self.indicator setFrame:self.bounds]; + +} + + +@end + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITAlertAction.h b/submodules/HockeySDK-iOS/Classes/BITAlertAction.h new file mode 100644 index 0000000000..caf1277f5c --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITAlertAction.h @@ -0,0 +1,9 @@ +#import + +@interface BITAlertAction : UIAlertAction + ++ (UIAlertAction * _Nonnull)actionWithTitle:(nullable NSString *)title style:(UIAlertActionStyle)style handler:(void (^_Nullable)(UIAlertAction *_Nonnull))handler; + +- (void)invokeAction; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITAlertAction.m b/submodules/HockeySDK-iOS/Classes/BITAlertAction.m new file mode 100644 index 0000000000..d689a5a960 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITAlertAction.m @@ -0,0 +1,23 @@ +#import "BITAlertAction.h" + +@interface BITAlertAction () + +@property (nonatomic, copy) void (^storedHandler)(UIAlertAction * _Nonnull); + +@end + +@implementation BITAlertAction + ++ (UIAlertAction *)actionWithTitle:(nullable NSString *)title style:(UIAlertActionStyle)style handler:(void (^)(UIAlertAction *_Nonnull))handler { + BITAlertAction *action = [super actionWithTitle:title style:style handler:handler]; + action.storedHandler = handler; + return action; +} + +- (void)invokeAction { + if (self.storedHandler) { + self.storedHandler(self); + } +} + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITAppStoreHeader.h b/submodules/HockeySDK-iOS/Classes/BITAppStoreHeader.h new file mode 100644 index 0000000000..c0b5e8f955 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITAppStoreHeader.h @@ -0,0 +1,40 @@ +/* + * Author: Andreas Linde + * Peter Steinberger + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011-2012 Peter Steinberger. + * 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 + +@interface BITAppStoreHeader : UIView + +@property (nonatomic, copy) NSString *headerText; +@property (nonatomic, copy) NSString *subHeaderText; +@property (nonatomic, strong) UIImage *iconImage; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITAppStoreHeader.m b/submodules/HockeySDK-iOS/Classes/BITAppStoreHeader.m new file mode 100644 index 0000000000..54e72abf76 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITAppStoreHeader.m @@ -0,0 +1,150 @@ +/* + * Author: Andreas Linde + * Peter Steinberger + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011-2012 Peter Steinberger. + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_UPDATES + +#import "BITAppStoreHeader.h" +#import "BITHockeyHelper.h" +#import "HockeySDKPrivate.h" + +#define kDarkGrayColor BIT_RGBCOLOR(186, 186, 186) +#define kWhiteBackgroundColorDefault BIT_RGBCOLOR(245, 245, 245) +#define kWhiteBackgroundColorOS7 BIT_RGBCOLOR(255, 255, 255) +#define kImageHeight 72 +#define kImageBorderRadiusiOS7 16.5 +#define kImageLeftMargin 14 +#define kImageTopMargin 12 +#define kTextRow kImageTopMargin*2 + kImageHeight + +@interface BITAppStoreHeader () + +@property (nonatomic, strong) UILabel *headerLabelView; +@property (nonatomic, strong) UILabel *middleLabelView; + +@end + +@implementation BITAppStoreHeader + + +#pragma mark - NSObject + +- (instancetype)initWithFrame:(CGRect)frame { + if ((self = [super initWithFrame:frame])) { + self.autoresizingMask = UIViewAutoresizingFlexibleWidth; + self.backgroundColor = kWhiteBackgroundColorDefault; + } + return self; +} + + +#pragma mark - UIView + +- (void)drawRect:(CGRect)rect { + CGRect bounds = self.bounds; + + // draw the line + CGContextRef ctx = UIGraphicsGetCurrentContext(); + CGContextSetLineWidth(ctx, 1.0); + CGContextSetStrokeColorWithColor(ctx, kDarkGrayColor.CGColor); + CGContextMoveToPoint(ctx, 0, CGRectGetMaxY(bounds)); + CGContextAddLineToPoint( ctx, CGRectGetMaxX(bounds), CGRectGetMaxY(bounds)); + CGContextStrokePath(ctx); + + // icon + [self.iconImage drawAtPoint:CGPointMake(kImageLeftMargin, kImageTopMargin)]; + + [super drawRect:rect]; +} + + +- (void)layoutSubviews { + self.backgroundColor = kWhiteBackgroundColorOS7; + + [super layoutSubviews]; + + CGFloat globalWidth = self.frame.size.width; + + // draw header name + UIColor *mainTextColor = BIT_RGBCOLOR(61, 61, 61); + UIColor *secondaryTextColor = BIT_RGBCOLOR(100, 100, 100); + UIFont *mainFont = [UIFont boldSystemFontOfSize:15]; + UIFont *secondaryFont = [UIFont systemFontOfSize:10]; + + if (!self.headerLabelView) self.headerLabelView = [[UILabel alloc] init]; + [self.headerLabelView setFont:mainFont]; + [self.headerLabelView setFrame:CGRectMake(kTextRow, kImageTopMargin, globalWidth-kTextRow, 20)]; + [self.headerLabelView setTextColor:mainTextColor]; + [self.headerLabelView setBackgroundColor:[UIColor clearColor]]; + [self.headerLabelView setText:self.headerText]; + [self addSubview:self.headerLabelView]; + + // middle + if (!self.middleLabelView) self.middleLabelView = [[UILabel alloc] init]; + [self.middleLabelView setFont:secondaryFont]; + [self.middleLabelView setFrame:CGRectMake(kTextRow, kImageTopMargin + 17, globalWidth-kTextRow, 20)]; + [self.middleLabelView setTextColor:secondaryTextColor]; + [self.middleLabelView setBackgroundColor:[UIColor clearColor]]; + [self.middleLabelView setText:self.subHeaderText]; + [self addSubview:self.middleLabelView]; +} + + +#pragma mark - Properties + +- (void)setHeaderText:(NSString *)anHeaderText { + if (_headerText != anHeaderText) { + _headerText = [anHeaderText copy]; + [self setNeedsDisplay]; + } +} + +- (void)setSubHeaderText:(NSString *)aSubHeaderText { + if (_subHeaderText != aSubHeaderText) { + _subHeaderText = [aSubHeaderText copy]; + [self setNeedsDisplay]; + } +} + +- (void)setIconImage:(UIImage *)anIconImage { + if (_iconImage != anIconImage) { + + // scale, make borders and reflection + _iconImage = bit_imageToFitSize(anIconImage, CGSizeMake(kImageHeight, kImageHeight), YES); + _iconImage = bit_roundedCornerImage(_iconImage, kImageBorderRadiusiOS7, 0.0); + + [self setNeedsDisplay]; + } +} + +@end + +#endif /* HOCKEYSDK_FEATURE_UPDATES */ diff --git a/submodules/HockeySDK-iOS/Classes/BITAppVersionMetaInfo.h b/submodules/HockeySDK-iOS/Classes/BITAppVersionMetaInfo.h new file mode 100644 index 0000000000..2143e02a72 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITAppVersionMetaInfo.h @@ -0,0 +1,58 @@ +/* + * Author: Peter Steinberger + * Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde, Peter Steinberger. + * 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 + +@interface BITAppVersionMetaInfo : NSObject { +} +@property (nonatomic, copy) NSString *name; +@property (nonatomic, copy) NSString *version; +@property (nonatomic, copy) NSString *shortVersion; +@property (nonatomic, copy) NSString *minOSVersion; +@property (nonatomic, copy) NSString *notes; +@property (nonatomic, copy) NSDate *date; +@property (nonatomic, copy) NSNumber *size; +@property (nonatomic, copy) NSNumber *mandatory; +@property (nonatomic, copy) NSNumber *versionID; +@property (nonatomic, copy) NSDictionary *uuids; + +- (NSString *)nameAndVersionString; +- (NSString *)versionString; +- (NSString *)dateString; +- (NSString *)sizeInMB; +- (NSString *)notesOrEmptyString; +- (void)setDateWithTimestamp:(NSTimeInterval)timestamp; +- (BOOL)isValid; +- (BOOL)hasUUID:(NSString *)uuid; +- (BOOL)isEqualToAppVersionMetaInfo:(BITAppVersionMetaInfo *)anAppVersionMetaInfo; + ++ (BITAppVersionMetaInfo *)appVersionMetaInfoFromDict:(NSDictionary *)dict; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITAppVersionMetaInfo.m b/submodules/HockeySDK-iOS/Classes/BITAppVersionMetaInfo.m new file mode 100644 index 0000000000..3f4906e020 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITAppVersionMetaInfo.m @@ -0,0 +1,228 @@ +/* + * Author: Peter Steinberger + * Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde, Peter Steinberger. + * 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 "BITAppVersionMetaInfo.h" +#import "HockeySDKPrivate.h" + + +@implementation BITAppVersionMetaInfo + + +#pragma mark - Static + ++ (BITAppVersionMetaInfo *)appVersionMetaInfoFromDict:(NSDictionary *)dict { + BITAppVersionMetaInfo *appVersionMetaInfo = [[[self class] alloc] init]; + + if ([dict isKindOfClass:[NSDictionary class]]) { + appVersionMetaInfo.name = [dict objectForKey:@"title"]; + appVersionMetaInfo.version = [dict objectForKey:@"version"]; + appVersionMetaInfo.shortVersion = [dict objectForKey:@"shortversion"]; + appVersionMetaInfo.minOSVersion = [dict objectForKey:@"minimum_os_version"]; + [appVersionMetaInfo setDateWithTimestamp:[[dict objectForKey:@"timestamp"] doubleValue]]; + appVersionMetaInfo.size = [dict objectForKey:@"appsize"]; + appVersionMetaInfo.notes = [dict objectForKey:@"notes"]; + appVersionMetaInfo.mandatory = [dict objectForKey:@"mandatory"]; + appVersionMetaInfo.versionID = [dict objectForKey:@"id"]; + appVersionMetaInfo.uuids = [dict objectForKey:@"uuids"]; + } + + return appVersionMetaInfo; +} + + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)other { + if (other == self) + return YES; + if (!other || ![other isKindOfClass:[self class]]) + return NO; + return [self isEqualToAppVersionMetaInfo:other]; +} + +- (BOOL)isEqualComparingString:(NSString *)stringA withString:(NSString *)stringB { + if ([stringA isKindOfClass:[NSString class]] && [stringB isKindOfClass:[NSString class]]) { + return [stringA isEqualToString:stringB]; + } + + return NO; +} + +- (BOOL)isEqualComparingNumber:(NSNumber *)numberA withNumber:(NSNumber *)numberB { + if ([numberA isKindOfClass:[NSNumber class]] && [numberB isKindOfClass:[NSNumber class]]) { + return [numberA isEqualToNumber:numberB]; + } + + return NO; +} + +- (BOOL)isEqualComparingDate:(NSDate *)dateA withDate:(NSDate *)dateB { + if ([dateA isKindOfClass:[NSDate class]] && [dateB isKindOfClass:[NSDate class]]) { + return [dateA isEqualToDate:dateB]; + } + + return NO; +} + +- (BOOL)isEqualComparingDictionary:(NSDictionary *)dictA withDate:(NSDictionary *)dictB { + if ([dictA isKindOfClass:[NSDictionary class]] && [dictB isKindOfClass:[NSDictionary class]]) { + return [dictA isEqualToDictionary:dictB]; + } + + return NO; +} + +- (BOOL)isEqualToAppVersionMetaInfo:(BITAppVersionMetaInfo *)anAppVersionMetaInfo { + if (self == anAppVersionMetaInfo) + return YES; + if (![self isEqualComparingString:self.name withString:anAppVersionMetaInfo.name]) + return NO; + if (![self isEqualComparingString:self.version withString:anAppVersionMetaInfo.version]) + return NO; + if (![self isEqualComparingString:self.shortVersion withString:anAppVersionMetaInfo.shortVersion]) + return NO; + if (![self isEqualComparingString:self.minOSVersion withString:anAppVersionMetaInfo.minOSVersion]) + return NO; + if (![self isEqualComparingString:self.notes withString:anAppVersionMetaInfo.notes]) + return NO; + if (![self isEqualComparingDate:self.date withDate:anAppVersionMetaInfo.date]) + return NO; + if (![self isEqualComparingNumber:self.size withNumber:anAppVersionMetaInfo.size]) + return NO; + if (![self isEqualComparingNumber:self.mandatory withNumber:anAppVersionMetaInfo.mandatory]) + return NO; + if (![self isEqualComparingDictionary:self.uuids withDate:anAppVersionMetaInfo.uuids]) + return NO; + return YES; +} + + +#pragma mark - NSCoder + +- (void)encodeWithCoder:(NSCoder *)encoder { + [encoder encodeObject:self.name forKey:@"name"]; + [encoder encodeObject:self.version forKey:@"version"]; + [encoder encodeObject:self.shortVersion forKey:@"shortVersion"]; + [encoder encodeObject:self.minOSVersion forKey:@"minOSVersion"]; + [encoder encodeObject:self.notes forKey:@"notes"]; + [encoder encodeObject:self.date forKey:@"date"]; + [encoder encodeObject:self.size forKey:@"size"]; + [encoder encodeObject:self.mandatory forKey:@"mandatory"]; + [encoder encodeObject:self.versionID forKey:@"versionID"]; + [encoder encodeObject:self.uuids forKey:@"uuids"]; +} + +- (instancetype)initWithCoder:(NSCoder *)decoder { + if ((self = [super init])) { + self.name = [decoder decodeObjectForKey:@"name"]; + self.version = [decoder decodeObjectForKey:@"version"]; + self.shortVersion = [decoder decodeObjectForKey:@"shortVersion"]; + self.minOSVersion = [decoder decodeObjectForKey:@"minOSVersion"]; + self.notes = [decoder decodeObjectForKey:@"notes"]; + self.date = [decoder decodeObjectForKey:@"date"]; + self.size = [decoder decodeObjectForKey:@"size"]; + self.mandatory = [decoder decodeObjectForKey:@"mandatory"]; + self.versionID = [decoder decodeObjectForKey:@"versionID"]; + self.uuids = [decoder decodeObjectForKey:@"uuids"]; + } + return self; +} + + +#pragma mark - Properties + +- (NSString *)nameAndVersionString { + NSString *appNameAndVersion = [NSString stringWithFormat:@"%@ %@", self.name, [self versionString]]; + return appNameAndVersion; +} + +- (NSString *)versionString { + NSString *shortString = ([self.shortVersion respondsToSelector:@selector(length)] && [self.shortVersion length]) ? [NSString stringWithFormat:@"%@", self.shortVersion] : @""; + NSString *versionString = [shortString length] ? [NSString stringWithFormat:@" (%@)", self.version] : self.version; + return [NSString stringWithFormat:@"%@ %@%@", BITHockeyLocalizedString(@"UpdateVersion"), shortString, versionString]; +} + +- (NSString *)dateString { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + [formatter setDateStyle:NSDateFormatterMediumStyle]; + + return [formatter stringFromDate:self.date]; +} + +- (NSString *)sizeInMB { + if ([self.size isKindOfClass: [NSNumber class]] && [self.size doubleValue] > 0) { + double appSizeInMB = [self.size doubleValue]/(1024*1024); + NSString *appSizeString = [NSString stringWithFormat:@"%.1f MB", appSizeInMB]; + return appSizeString; + } + + return @"0 MB"; +} + +- (void)setDateWithTimestamp:(NSTimeInterval)timestamp { + if (timestamp != 0) { + NSDate *appDate = [NSDate dateWithTimeIntervalSince1970:timestamp]; + self.date = appDate; + } else { + self.date = nil; + } +} + +- (NSString *)notesOrEmptyString { + if (self.notes) { + return self.notes; + }else { + return [NSString string]; + } +} + + +// A valid app needs at least following properties: name, version, date +- (BOOL)isValid { + BOOL valid = [self.name length] && [self.version length] && self.date; + return valid; +} + +- (BOOL)hasUUID:(NSString *)uuid { + if (!uuid) return NO; + if (!self.uuids) return NO; + + __block BOOL hasUUID = NO; + + [self.uuids enumerateKeysAndObjectsUsingBlock:^(id __unused key, id obj, BOOL *stop){ + if (obj && [uuid compare:obj] == NSOrderedSame) { + hasUUID = YES; + *stop = YES; + } + }]; + + return hasUUID; +} +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITApplication.h b/submodules/HockeySDK-iOS/Classes/BITApplication.h new file mode 100755 index 0000000000..6a03147e2d --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITApplication.h @@ -0,0 +1,12 @@ +#import "BITTelemetryObject.h" + +@interface BITApplication : BITTelemetryObject + +@property (nonatomic, copy) NSString *version; +@property (nonatomic, copy) NSString *build; +@property (nonatomic, copy) NSString *typeId; + +- (instancetype)initWithCoder:(NSCoder *)coder; +- (void)encodeWithCoder:(NSCoder *)coder; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITApplication.m b/submodules/HockeySDK-iOS/Classes/BITApplication.m new file mode 100755 index 0000000000..e3104f3616 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITApplication.m @@ -0,0 +1,44 @@ +#import "BITApplication.h" + +/// Data contract class for type Application. +@implementation BITApplication + +/// +/// Adds all members of this class to a dictionary +/// +- (NSDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary].mutableCopy; + if (self.version != nil) { + [dict setObject:self.version forKey:@"ai.application.ver"]; + } + if (self.build != nil) { + [dict setObject:self.build forKey:@"ai.application.build"]; + } + if (self.typeId != nil) { + [dict setObject:self.typeId forKey:@"ai.application.typeId"]; + } + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _version = [coder decodeObjectForKey:@"self.version"]; + _build = [coder decodeObjectForKey:@"self.build"]; + _typeId = [coder decodeObjectForKey:@"self.typeId"]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.version forKey:@"self.version"]; + [coder encodeObject:self.build forKey:@"self.build"]; + [coder encodeObject:self.typeId forKey:@"self.typeId"]; +} + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITArrowImageAnnotation.h b/submodules/HockeySDK-iOS/Classes/BITArrowImageAnnotation.h new file mode 100644 index 0000000000..feeca7613b --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITArrowImageAnnotation.h @@ -0,0 +1,33 @@ +/* + * Author: Moritz Haarmann + * + * 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 "BITImageAnnotation.h" + +@interface BITArrowImageAnnotation : BITImageAnnotation + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITArrowImageAnnotation.m b/submodules/HockeySDK-iOS/Classes/BITArrowImageAnnotation.m new file mode 100644 index 0000000000..d74a76cf10 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITArrowImageAnnotation.m @@ -0,0 +1,215 @@ +/* + * Author: Moritz Haarmann + * + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "BITArrowImageAnnotation.h" +#import + +#define kArrowPointCount 7 + + +@interface BITArrowImageAnnotation() + +@property (nonatomic, strong) CAShapeLayer *shapeLayer; +@property (nonatomic, strong) CAShapeLayer *strokeLayer; + +@end + +@implementation BITArrowImageAnnotation + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.shapeLayer = [CAShapeLayer layer]; + self.shapeLayer.strokeColor = [UIColor whiteColor].CGColor; + self.shapeLayer.lineWidth = 5; + self.shapeLayer.fillColor = [UIColor redColor].CGColor; + + self.strokeLayer = [CAShapeLayer layer]; + self.strokeLayer.strokeColor = [UIColor redColor].CGColor; + self.strokeLayer.lineWidth = 10; + self.strokeLayer.fillColor = [UIColor clearColor].CGColor; + [self.layer addSublayer:self.strokeLayer]; + + [self.layer addSublayer:self.shapeLayer]; + + + } + return self; +} + +- (void)buildShape { + CGFloat baseWidth = MAX(self.frame.size.width, self.frame.size.height); + CGFloat topHeight = MAX(baseWidth / 3,10); + + + CGFloat lineWidth = MAX(baseWidth / 10,3); + CGFloat startX, startY, endX, endY; + + CGRect boundRect = CGRectInset(self.bounds, 0, 0); + CGFloat arrowLength= sqrt(pow(CGRectGetWidth(boundRect), (CGFloat)2) + pow(CGRectGetHeight(boundRect), (CGFloat)2)); + if (0 < arrowLength && arrowLength < 30){ + + CGFloat factor = 30 / arrowLength; + + boundRect = CGRectApplyAffineTransform(boundRect, CGAffineTransformMakeScale(factor,factor)); + } + + if ( self.movedDelta.width < 0){ + startX = CGRectGetMinX(boundRect); + endX = CGRectGetMaxX(boundRect); + } else { + startX = CGRectGetMaxX(boundRect); + endX = CGRectGetMinX(boundRect); + + } + + if ( self.movedDelta.height < 0){ + startY = CGRectGetMinY(boundRect); + endY = CGRectGetMaxY(boundRect); + } else { + startY = CGRectGetMaxY(boundRect); + endY = CGRectGetMinY(boundRect); + + } + + + if (fabs(CGRectGetWidth(boundRect)) < 30 || fabs(CGRectGetHeight(boundRect)) < 30){ + CGFloat smallerOne = MIN(fabs(CGRectGetHeight(boundRect)), fabs(CGRectGetWidth(boundRect))); + + CGFloat factor = smallerOne / 30; + + CGRectApplyAffineTransform(boundRect, CGAffineTransformMakeScale(factor,factor)); + } + + UIBezierPath *path = [self bezierPathWithArrowFromPoint:CGPointMake(endX, endY) toPoint:CGPointMake(startX, startY) tailWidth:lineWidth headWidth:topHeight headLength:topHeight]; + + self.shapeLayer.path = path.CGPath; + self.strokeLayer.path = path.CGPath; + [CATransaction begin]; + [CATransaction setAnimationDuration:0]; + self.strokeLayer.lineWidth = lineWidth / (CGFloat)1.5; + self.shapeLayer.lineWidth = lineWidth / 3; + + [CATransaction commit]; + +} + + +- (UIBezierPath *)bezierPathWithArrowFromPoint:(CGPoint)startPoint + toPoint:(CGPoint)endPoint + tailWidth:(CGFloat)tailWidth + headWidth:(CGFloat)headWidth + headLength:(CGFloat)headLength { + CGFloat length = hypot(endPoint.x - startPoint.x, endPoint.y - startPoint.y); + + CGPoint points[kArrowPointCount]; + [self getAxisAlignedArrowPoints:points + forLength:length + tailWidth:tailWidth + headWidth:headWidth + headLength:headLength]; + + CGAffineTransform transform = [self transformForStartPoint:startPoint + endPoint:endPoint + length:length]; + + CGMutablePathRef cgPath = CGPathCreateMutable(); + CGPathAddLines(cgPath, &transform, points, sizeof points / sizeof *points); + CGPathCloseSubpath(cgPath); + + UIBezierPath *uiPath = [UIBezierPath bezierPathWithCGPath:cgPath]; + CGPathRelease(cgPath); + return uiPath; +} + +- (void)getAxisAlignedArrowPoints:(CGPoint[kArrowPointCount])points + forLength:(CGFloat)length + tailWidth:(CGFloat)tailWidth + headWidth:(CGFloat)headWidth + headLength:(CGFloat)headLength { + CGFloat tailLength = length - headLength; + points[0] = CGPointMake(0, tailWidth / 2); + points[1] = CGPointMake(tailLength, tailWidth / 2); + points[2] = CGPointMake(tailLength, headWidth / 2); + points[3] = CGPointMake(length, 0); + points[4] = CGPointMake(tailLength, -headWidth / 2); + points[5] = CGPointMake(tailLength, -tailWidth / 2); + points[6] = CGPointMake(0, -tailWidth / 2); +} + ++ (CGAffineTransform)dqd_transformForStartPoint:(CGPoint)startPoint + endPoint:(CGPoint)endPoint + length:(CGFloat)length { + CGFloat cosine = (endPoint.x - startPoint.x) / length; + CGFloat sine = (endPoint.y - startPoint.y) / length; + return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y }; +} + +- (CGAffineTransform)transformForStartPoint:(CGPoint)startPoint + endPoint:(CGPoint)endPoint + length:(CGFloat)length { + if (CGPointEqualToPoint(startPoint, CGPointZero) && (length == 0)) { + return CGAffineTransformIdentity; + } + + CGFloat cosine = (endPoint.x - startPoint.x) / length; + CGFloat sine = (endPoint.y - startPoint.y) / length; + return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y }; +} + +#pragma mark - UIView + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *) __unused event { + + CGPathRef strokePath = CGPathCreateCopyByStrokingPath(self.shapeLayer.path, NULL, fmax(90.0f, self.shapeLayer.lineWidth), kCGLineCapRound,kCGLineJoinMiter,0); + + BOOL containsPoint = CGPathContainsPoint(strokePath, NULL, point, NO); + + CGPathRelease(strokePath); + + if (containsPoint){ + return self; + } else { + return nil; + } + +} + +- (void)layoutSubviews{ + [super layoutSubviews]; + + [self buildShape]; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITAttributedLabel.h b/submodules/HockeySDK-iOS/Classes/BITAttributedLabel.h new file mode 100644 index 0000000000..720aaf37ef --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITAttributedLabel.h @@ -0,0 +1,681 @@ +// TTTAttributedLabel.h +// +// Copyright (c) 2011 Mattt Thompson (http://mattt.me) +// +// 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 +#import + +//! Project version number for BITAttributedLabel. +FOUNDATION_EXPORT double BITAttributedLabelVersionNumber; + +//! Project version string for BITAttributedLabel. +FOUNDATION_EXPORT const unsigned char BITAttributedLabelVersionString[]; + +@class BITAttributedLabelLink; + +/** + Vertical alignment for text in a label whose bounds are larger than its text bounds + */ +typedef NS_ENUM(NSInteger, BITAttributedLabelVerticalAlignment) { + BITAttributedLabelVerticalAlignmentCenter = 0, + BITAttributedLabelVerticalAlignmentTop = 1, + BITAttributedLabelVerticalAlignmentBottom = 2, +}; + +/** + Determines whether the text to which this attribute applies has a strikeout drawn through itself. + */ +extern NSString * const kBITStrikeOutAttributeName; + +/** + The background fill color. Value must be a `CGColorRef`. Default value is `nil` (no fill). + */ +extern NSString * const kBITBackgroundFillColorAttributeName; + +/** + The padding for the background fill. Value must be a `UIEdgeInsets`. Default value is `UIEdgeInsetsZero` (no padding). + */ +extern NSString * const kBITBackgroundFillPaddingAttributeName; + +/** + The background stroke color. Value must be a `CGColorRef`. Default value is `nil` (no stroke). + */ +extern NSString * const kBITBackgroundStrokeColorAttributeName; + +/** + The background stroke line width. Value must be an `NSNumber`. Default value is `1.0f`. + */ +extern NSString * const kBITBackgroundLineWidthAttributeName; + +/** + The background corner radius. Value must be an `NSNumber`. Default value is `5.0f`. + */ +extern NSString * const kBITBackgroundCornerRadiusAttributeName; + +@protocol BITAttributedLabelDelegate; + +// Override UILabel @property to accept both NSString and NSAttributedString +@protocol BITAttributedLabel +@property (nonatomic, copy) IBInspectable id text; +@end + +IB_DESIGNABLE + +/** + `BITAttributedLabel` is a drop-in replacement for `UILabel` that supports `NSAttributedString`, as well as automatically-detected and manually-added links to URLs, addresses, phone numbers, and dates. + + ## Differences Between `BITAttributedLabel` and `UILabel` + + For the most part, `BITAttributedLabel` behaves just like `UILabel`. The following are notable exceptions, in which `BITAttributedLabel` may act differently: + + - `text` - This property now takes an `id` type argument, which can either be a kind of `NSString` or `NSAttributedString` (mutable or immutable in both cases) + - `attributedText` - Do not set this property directly. Instead, pass an `NSAttributedString` to `text`. + - `lineBreakMode` - This property displays only the first line when the value is `UILineBreakModeHeadTruncation`, `UILineBreakModeTailTruncation`, or `UILineBreakModeMiddleTruncation` + - `adjustsFontsizeToFitWidth` - Supported in iOS 5 and greater, this property is effective for any value of `numberOfLines` greater than zero. In iOS 4, setting `numberOfLines` to a value greater than 1 with `adjustsFontSizeToFitWidth` set to `YES` may cause `sizeToFit` to execute indefinitely. + - `baselineAdjustment` - This property has no affect. + - `textAlignment` - This property does not support justified alignment. + - `NSTextAttachment` - This string attribute is not supported. + + Any properties affecting text or paragraph styling, such as `firstLineIndent` will only apply when text is set with an `NSString`. If the text is set with an `NSAttributedString`, these properties will not apply. + + ### NSCoding + + `BITAttributedLabel`, like `UILabel`, conforms to `NSCoding`. However, if the build target is set to less than iOS 6.0, `linkAttributes` and `activeLinkAttributes` will not be encoded or decoded. This is due to an runtime exception thrown when attempting to copy non-object CoreText values in dictionaries. + + @warning Any properties changed on the label after setting the text will not be reflected until a subsequent call to `setText:` or `setText:afterInheritingLabelAttributesAndConfiguringWithBlock:`. This is to say, order of operations matters in this case. For example, if the label text color is originally black when the text is set, changing the text color to red will have no effect on the display of the label until the text is set once again. + + @bug Setting `attributedText` directly is not recommended, as it may cause a crash when attempting to access any links previously set. Instead, call `setText:`, passing an `NSAttributedString`. + */ +@interface BITAttributedLabel : UILabel + +/** + * The designated initializers are @c initWithFrame: and @c initWithCoder:. + * init will not properly initialize many required properties and other configuration. + */ +- (instancetype) init NS_UNAVAILABLE; + +///----------------------------- +/// @name Accessing the Delegate +///----------------------------- + +/** + The receiver's delegate. + + @discussion A `BITAttributedLabel` delegate responds to messages sent by tapping on links in the label. You can use the delegate to respond to links referencing a URL, address, phone number, date, or date with a specified time zone and duration. + */ +@property (nonatomic, unsafe_unretained) IBOutlet id delegate; + +///-------------------------------------------- +/// @name Detecting, Accessing, & Styling Links +///-------------------------------------------- + +/** + A bitmask of `NSTextCheckingType` which are used to automatically detect links in the label text. + + @warning You must specify `enabledTextCheckingTypes` before setting the `text`, with either `setText:` or `setText:afterInheritingLabelAttributesAndConfiguringWithBlock:`. + */ +@property (nonatomic, assign) NSTextCheckingTypes enabledTextCheckingTypes; + +/** + An array of `NSTextCheckingResult` objects for links detected or manually added to the label text. + */ +@property (readonly, nonatomic, strong) NSArray *links; + +/** + A dictionary containing the default `NSAttributedString` attributes to be applied to links detected or manually added to the label text. The default link style is blue and underlined. + + @warning You must specify `linkAttributes` before setting autodecting or manually-adding links for these attributes to be applied. + */ +@property (nonatomic, strong) NSDictionary *linkAttributes; + +/** + A dictionary containing the default `NSAttributedString` attributes to be applied to links when they are in the active state. If `nil` or an empty `NSDictionary`, active links will not be styled. The default active link style is red and underlined. + */ +@property (nonatomic, strong) NSDictionary *activeLinkAttributes; + +/** + A dictionary containing the default `NSAttributedString` attributes to be applied to links when they are in the inactive state, which is triggered by a change in `tintColor` in iOS 7 and later. If `nil` or an empty `NSDictionary`, inactive links will not be styled. The default inactive link style is gray and unadorned. + */ +@property (nonatomic, strong) NSDictionary *inactiveLinkAttributes; + +/** + The edge inset for the background of a link. The default value is `{0, -1, 0, -1}`. + */ +@property (nonatomic, assign) UIEdgeInsets linkBackgroundEdgeInset; + +/** + Indicates if links will be detected within an extended area around the touch + to emulate the link detection behaviour of UIWebView. + Default value is NO. Enabling this may adversely impact performance. + */ +@property (nonatomic, assign) BOOL extendsLinkTouchArea; + +///--------------------------------------- +/// @name Acccessing Text Style Attributes +///--------------------------------------- + +/** + The shadow blur radius for the label. A value of 0 indicates no blur, while larger values produce correspondingly larger blurring. This value must not be negative. The default value is 0. + */ +@property (nonatomic, assign) IBInspectable CGFloat shadowRadius; + +/** + The shadow blur radius for the label when the label's `highlighted` property is `YES`. A value of 0 indicates no blur, while larger values produce correspondingly larger blurring. This value must not be negative. The default value is 0. + */ +@property (nonatomic, assign) IBInspectable CGFloat highlightedShadowRadius; +/** + The shadow offset for the label when the label's `highlighted` property is `YES`. A size of {0, 0} indicates no offset, with positive values extending down and to the right. The default size is {0, 0}. + */ +@property (nonatomic, assign) IBInspectable CGSize highlightedShadowOffset; +/** + The shadow color for the label when the label's `highlighted` property is `YES`. The default value is `nil` (no shadow color). + */ +@property (nonatomic, strong) IBInspectable UIColor *highlightedShadowColor; + +/** + The amount to kern the next character. Default is standard kerning. If this attribute is set to 0.0, no kerning is done at all. + */ +@property (nonatomic, assign) IBInspectable CGFloat kern; + +///-------------------------------------------- +/// @name Acccessing Paragraph Style Attributes +///-------------------------------------------- + +/** + The distance, in points, from the leading margin of a frame to the beginning of the + paragraph's first line. This value is always nonnegative, and is 0.0 by default. + This applies to the full text, rather than any specific paragraph metrics. + */ +@property (nonatomic, assign) IBInspectable CGFloat firstLineIndent; + +/** + The space in points added between lines within the paragraph. This value is always nonnegative and is 0.0 by default. + */ +@property (nonatomic, assign) IBInspectable CGFloat lineSpacing; + +/** + The minimum line height within the paragraph. If the value is 0.0, the minimum line height is set to the line height of the `font`. 0.0 by default. + */ +@property (nonatomic, assign) IBInspectable CGFloat minimumLineHeight; + +/** + The maximum line height within the paragraph. If the value is 0.0, the maximum line height is set to the line height of the `font`. 0.0 by default. + */ +@property (nonatomic, assign) IBInspectable CGFloat maximumLineHeight; + +/** + The line height multiple. This value is 1.0 by default. + */ +@property (nonatomic, assign) IBInspectable CGFloat lineHeightMultiple; + +/** + The distance, in points, from the margin to the text container. This value is `UIEdgeInsetsZero` by default. + sizeThatFits: will have its returned size increased by these margins. + drawTextInRect: will inset all drawn text by these margins. + */ +@property (nonatomic, assign) IBInspectable UIEdgeInsets textInsets; + +/** + The vertical text alignment for the label, for when the frame size is greater than the text rect size. The vertical alignment is `BITAttributedLabelVerticalAlignmentCenter` by default. + */ +@property (nonatomic, assign) BITAttributedLabelVerticalAlignment verticalAlignment; + +///-------------------------------------------- +/// @name Accessing Truncation Token Appearance +///-------------------------------------------- + +/** + The attributed string to apply to the truncation token at the end of a truncated line. + */ +@property (nonatomic, strong) IBInspectable NSAttributedString *attributedTruncationToken; + +///-------------------------- +/// @name Long press gestures +///-------------------------- + +/** + * The long-press gesture recognizer used internally by the label. + */ +@property (nonatomic, strong, readonly) UILongPressGestureRecognizer *longPressGestureRecognizer; + +///-------------------------------------------- +/// @name Calculating Size of Attributed String +///-------------------------------------------- + +/** + Calculate and return the size that best fits an attributed string, given the specified constraints on size and number of lines. + + @param attributedString The attributed string. + @param size The maximum dimensions used to calculate size. + @param numberOfLines The maximum number of lines in the text to draw, if the constraining size cannot accomodate the full attributed string. + + @return The size that fits the attributed string within the specified constraints. + */ ++ (CGSize)sizeThatFitsAttributedString:(NSAttributedString *)attributedString + withConstraints:(CGSize)size + limitedToNumberOfLines:(NSUInteger)numberOfLines; + +///---------------------------------- +/// @name Setting the Text Attributes +///---------------------------------- + +/** + Sets the text displayed by the label. + + @param text An `NSString` or `NSAttributedString` object to be displayed by the label. If the specified text is an `NSString`, the label will display the text like a `UILabel`, inheriting the text styles of the label. If the specified text is an `NSAttributedString`, the label text styles will be overridden by the styles specified in the attributed string. + + @discussion This method overrides `UILabel -setText:` to accept both `NSString` and `NSAttributedString` objects. This string is `nil` by default. + */ +- (void)setText:(id)text; + +/** + Sets the text displayed by the label, after configuring an attributed string containing the text attributes inherited from the label in a block. + + @param text An `NSString` or `NSAttributedString` object to be displayed by the label. + @param block A block object that returns an `NSMutableAttributedString` object and takes a single argument, which is an `NSMutableAttributedString` object with the text from the first parameter, and the text attributes inherited from the label text styles. For example, if you specified the `font` of the label to be `[UIFont boldSystemFontOfSize:14]` and `textColor` to be `[UIColor redColor]`, the `NSAttributedString` argument of the block would be contain the `NSAttributedString` attribute equivalents of those properties. In this block, you can set further attributes on particular ranges. + + @discussion This string is `nil` by default. + */ +- (void)setText:(id)text +afterInheritingLabelAttributesAndConfiguringWithBlock:(NSMutableAttributedString *(^)(NSMutableAttributedString *mutableAttributedString))block; + +///------------------------------------ +/// @name Accessing the Text Attributes +///------------------------------------ + +/** + A copy of the label's current attributedText. This returns `nil` if an attributed string has never been set on the label. + + @warning Do not set this property directly. Instead, set @c text to an @c NSAttributedString. + */ +@property (readwrite, nonatomic, copy) NSAttributedString *attributedText; + +///------------------- +/// @name Adding Links +///------------------- + +/** + Adds a link. You can customize an individual link's appearance and accessibility value by creating your own @c BITAttributedLabelLink and passing it to this method. The other methods for adding links will use the label's default attributes. + + @warning Modifying the link's attribute dictionaries must be done before calling this method. + + @param link A @c BITAttributedLabelLink object. + */ +- (void)addLink:(BITAttributedLabelLink *)link; + +/** + Adds a link to an @c NSTextCheckingResult. + + @param result An @c NSTextCheckingResult representing the link's location and type. + + @return The newly added link object. + */ +- (BITAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result; + +/** + Adds a link to an @c NSTextCheckingResult. + + @param result An @c NSTextCheckingResult representing the link's location and type. + @param attributes The attributes to be added to the text in the range of the specified link. If set, the label's @c activeAttributes and @c inactiveAttributes will be applied to the link. If `nil`, no attributes are added to the link. + + @return The newly added link object. + */ +- (BITAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result + attributes:(NSDictionary *)attributes; + +/** + Adds a link to a URL for a specified range in the label text. + + @param url The url to be linked to + @param range The range in the label text of the link. The range must not exceed the bounds of the receiver. + + @return The newly added link object. + */ +- (BITAttributedLabelLink *)addLinkToURL:(NSURL *)url + withRange:(NSRange)range; + +/** + Adds a link to an address for a specified range in the label text. + + @param addressComponents A dictionary of address components for the address to be linked to + @param range The range in the label text of the link. The range must not exceed the bounds of the receiver. + + @discussion The address component dictionary keys are described in `NSTextCheckingResult`'s "Keys for Address Components." + + @return The newly added link object. + */ +- (BITAttributedLabelLink *)addLinkToAddress:(NSDictionary *)addressComponents + withRange:(NSRange)range; + +/** + Adds a link to a phone number for a specified range in the label text. + + @param phoneNumber The phone number to be linked to. + @param range The range in the label text of the link. The range must not exceed the bounds of the receiver. + + @return The newly added link object. + */ +- (BITAttributedLabelLink *)addLinkToPhoneNumber:(NSString *)phoneNumber + withRange:(NSRange)range; + +/** + Adds a link to a date for a specified range in the label text. + + @param date The date to be linked to. + @param range The range in the label text of the link. The range must not exceed the bounds of the receiver. + + @return The newly added link object. + */ +- (BITAttributedLabelLink *)addLinkToDate:(NSDate *)date + withRange:(NSRange)range; + +/** + Adds a link to a date with a particular time zone and duration for a specified range in the label text. + + @param date The date to be linked to. + @param timeZone The time zone of the specified date. + @param duration The duration, in seconds from the specified date. + @param range The range in the label text of the link. The range must not exceed the bounds of the receiver. + + @return The newly added link object. + */ +- (BITAttributedLabelLink *)addLinkToDate:(NSDate *)date + timeZone:(NSTimeZone *)timeZone + duration:(NSTimeInterval)duration + withRange:(NSRange)range; + +/** + Adds a link to transit information for a specified range in the label text. + + @param components A dictionary containing the transit components. The currently supported keys are `NSTextCheckingAirlineKey` and `NSTextCheckingFlightKey`. + @param range The range in the label text of the link. The range must not exceed the bounds of the receiver. + + @return The newly added link object. + */ +- (BITAttributedLabelLink *)addLinkToTransitInformation:(NSDictionary *)components + withRange:(NSRange)range; + +/** + Returns whether an @c NSTextCheckingResult is found at the give point. + + @discussion This can be used together with @c UITapGestureRecognizer to tap interactions with overlapping views. + + @param point The point inside the label. + */ +- (BOOL)containslinkAtPoint:(CGPoint)point; + +/** + Returns the @c BITAttributedLabelLink at the give point if it exists. + + @discussion This can be used together with @c UIViewControllerPreviewingDelegate to peek into links. + + @param point The point inside the label. + */ +- (BITAttributedLabelLink *)linkAtPoint:(CGPoint)point; + +@end + +/** + The `BITAttributedLabelDelegate` protocol defines the messages sent to an attributed label delegate when links are tapped. All of the methods of this protocol are optional. + */ +@protocol BITAttributedLabelDelegate + +///----------------------------------- +/// @name Responding to Link Selection +///----------------------------------- +@optional + +/** + Tells the delegate that the user did select a link to a URL. + + @param label The label whose link was selected. + @param url The URL for the selected link. + */ +- (void)attributedLabel:(BITAttributedLabel *)label + didSelectLinkWithURL:(NSURL *)url; + +/** + Tells the delegate that the user did select a link to an address. + + @param label The label whose link was selected. + @param addressComponents The components of the address for the selected link. + */ +- (void)attributedLabel:(BITAttributedLabel *)label +didSelectLinkWithAddress:(NSDictionary *)addressComponents; + +/** + Tells the delegate that the user did select a link to a phone number. + + @param label The label whose link was selected. + @param phoneNumber The phone number for the selected link. + */ +- (void)attributedLabel:(BITAttributedLabel *)label +didSelectLinkWithPhoneNumber:(NSString *)phoneNumber; + +/** + Tells the delegate that the user did select a link to a date. + + @param label The label whose link was selected. + @param date The datefor the selected link. + */ +- (void)attributedLabel:(BITAttributedLabel *)label + didSelectLinkWithDate:(NSDate *)date; + +/** + Tells the delegate that the user did select a link to a date with a time zone and duration. + + @param label The label whose link was selected. + @param date The date for the selected link. + @param timeZone The time zone of the date for the selected link. + @param duration The duration, in seconds from the date for the selected link. + */ +- (void)attributedLabel:(BITAttributedLabel *)label + didSelectLinkWithDate:(NSDate *)date + timeZone:(NSTimeZone *)timeZone + duration:(NSTimeInterval)duration; + +/** + Tells the delegate that the user did select a link to transit information + + @param label The label whose link was selected. + @param components A dictionary containing the transit components. The currently supported keys are `NSTextCheckingAirlineKey` and `NSTextCheckingFlightKey`. + */ +- (void)attributedLabel:(BITAttributedLabel *)label +didSelectLinkWithTransitInformation:(NSDictionary *)components; + +/** + Tells the delegate that the user did select a link to a text checking result. + + @discussion This method is called if no other delegate method was called, which can occur by either now implementing the method in `BITAttributedLabelDelegate` corresponding to a particular link, or the link was added by passing an instance of a custom `NSTextCheckingResult` subclass into `-addLinkWithTextCheckingResult:`. + + @param label The label whose link was selected. + @param result The custom text checking result. + */ +- (void)attributedLabel:(BITAttributedLabel *)label +didSelectLinkWithTextCheckingResult:(NSTextCheckingResult *)result; + +///--------------------------------- +/// @name Responding to Long Presses +///--------------------------------- + +/** + * Long-press delegate methods include the CGPoint tapped within the label's coordinate space. + * This may be useful on iPad to present a popover from a specific origin point. + */ + +/** + Tells the delegate that the user long-pressed a link to a URL. + + @param label The label whose link was long pressed. + @param url The URL for the link. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(BITAttributedLabel *)label +didLongPressLinkWithURL:(NSURL *)url + atPoint:(CGPoint)point; + +/** + Tells the delegate that the user long-pressed a link to an address. + + @param label The label whose link was long pressed. + @param addressComponents The components of the address for the link. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(BITAttributedLabel *)label +didLongPressLinkWithAddress:(NSDictionary *)addressComponents + atPoint:(CGPoint)point; + +/** + Tells the delegate that the user long-pressed a link to a phone number. + + @param label The label whose link was long pressed. + @param phoneNumber The phone number for the link. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(BITAttributedLabel *)label +didLongPressLinkWithPhoneNumber:(NSString *)phoneNumber + atPoint:(CGPoint)point; + + +/** + Tells the delegate that the user long-pressed a link to a date. + + @param label The label whose link was long pressed. + @param date The date for the selected link. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(BITAttributedLabel *)label +didLongPressLinkWithDate:(NSDate *)date + atPoint:(CGPoint)point; + + +/** + Tells the delegate that the user long-pressed a link to a date with a time zone and duration. + + @param label The label whose link was long pressed. + @param date The date for the link. + @param timeZone The time zone of the date for the link. + @param duration The duration, in seconds from the date for the link. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(BITAttributedLabel *)label +didLongPressLinkWithDate:(NSDate *)date + timeZone:(NSTimeZone *)timeZone + duration:(NSTimeInterval)duration + atPoint:(CGPoint)point; + + +/** + Tells the delegate that the user long-pressed a link to transit information. + + @param label The label whose link was long pressed. + @param components A dictionary containing the transit components. The currently supported keys are `NSTextCheckingAirlineKey` and `NSTextCheckingFlightKey`. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(BITAttributedLabel *)label +didLongPressLinkWithTransitInformation:(NSDictionary *)components + atPoint:(CGPoint)point; + +/** + Tells the delegate that the user long-pressed a link to a text checking result. + + @discussion Similar to `-attributedLabel:didSelectLinkWithTextCheckingResult:`, this method is called if a link is long pressed and the delegate does not implement the method corresponding to this type of link. + + @param label The label whose link was long pressed. + @param result The custom text checking result. + @param point the point pressed, in the label's coordinate space + */ +- (void)attributedLabel:(BITAttributedLabel *)label +didLongPressLinkWithTextCheckingResult:(NSTextCheckingResult *)result + atPoint:(CGPoint)point; + +@end + +@interface BITAttributedLabelLink : NSObject + +typedef void (^BITAttributedLabelLinkBlock) (BITAttributedLabel *, BITAttributedLabelLink *); + +/** + An `NSTextCheckingResult` representing the link's location and type. + */ +@property (readonly, nonatomic, strong) NSTextCheckingResult *result; + +/** + A dictionary containing the @c NSAttributedString attributes to be applied to the link. + */ +@property (readonly, nonatomic, copy) NSDictionary *attributes; + +/** + A dictionary containing the @c NSAttributedString attributes to be applied to the link when it is in the active state. + */ +@property (readonly, nonatomic, copy) NSDictionary *activeAttributes; + +/** + A dictionary containing the @c NSAttributedString attributes to be applied to the link when it is in the inactive state, which is triggered by a change in `tintColor` in iOS 7 and later. + */ +@property (readonly, nonatomic, copy) NSDictionary *inactiveAttributes; + +/** + Additional information about a link for VoiceOver users. Has default values if the link's @c result is @c NSTextCheckingTypeLink, @c NSTextCheckingTypePhoneNumber, or @c NSTextCheckingTypeDate. + */ +@property (nonatomic, copy) NSString *accessibilityValue; + +/** + A block called when this link is tapped. + If non-nil, tapping on this link will call this block instead of the + @c BITAttributedLabelDelegate tap methods, which will not be called for this link. + */ +@property (nonatomic, copy) BITAttributedLabelLinkBlock linkTapBlock; + +/** + A block called when this link is long-pressed. + If non-nil, long pressing on this link will call this block instead of the + @c BITAttributedLabelDelegate long press methods, which will not be called for this link. + */ +@property (nonatomic, copy) BITAttributedLabelLinkBlock linkLongPressBlock; + +/** + Initializes a link using the attribute dictionaries specified. + + @param attributes The @c attributes property for the link. + @param activeAttributes The @c activeAttributes property for the link. + @param inactiveAttributes The @c inactiveAttributes property for the link. + @param result An @c NSTextCheckingResult representing the link's location and type. + + @return The initialized link object. + */ +- (instancetype)initWithAttributes:(NSDictionary *)attributes + activeAttributes:(NSDictionary *)activeAttributes + inactiveAttributes:(NSDictionary *)inactiveAttributes + textCheckingResult:(NSTextCheckingResult *)result; + +/** + Initializes a link using the attribute dictionaries set on a specified label. + + @param label The attributed label from which to inherit attribute dictionaries. + @param result An @c NSTextCheckingResult representing the link's location and type. + + @return The initialized link object. + */ +- (instancetype)initWithAttributesFromLabel:(BITAttributedLabel*)label + textCheckingResult:(NSTextCheckingResult *)result; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITAttributedLabel.m b/submodules/HockeySDK-iOS/Classes/BITAttributedLabel.m new file mode 100644 index 0000000000..0e48cea48b --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITAttributedLabel.m @@ -0,0 +1,1850 @@ +// TTTAttributedLabel.m +// +// Copyright (c) 2011 Mattt Thompson (http://mattt.me) +// +// 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "BITAttributedLabel.h" + +#import +#import +#import + +#define kBITLineBreakWordWrapTextWidthScalingFactor (M_PI / M_E) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wunused-variable" +#pragma clang diagnostic ignored "-Wunused-parameter" +#pragma clang diagnostic ignored "-Wcast-qual" +#pragma clang diagnostic ignored "-Wdouble-promotion" +#pragma clang diagnostic ignored "-Wdirect-ivar-access" +#pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion" +#pragma clang diagnostic ignored "-Wvla" + +static CGFloat const BITFLOAT_MAX = 100000; + +NSString * const kBITStrikeOutAttributeName = @"BITStrikeOutAttribute"; +NSString * const kBITBackgroundFillColorAttributeName = @"BITBackgroundFillColor"; +NSString * const kBITBackgroundFillPaddingAttributeName = @"BITBackgroundFillPadding"; +NSString * const kBITBackgroundStrokeColorAttributeName = @"BITBackgroundStrokeColor"; +NSString * const kBITBackgroundLineWidthAttributeName = @"BITBackgroundLineWidth"; +NSString * const kBITBackgroundCornerRadiusAttributeName = @"BITBackgroundCornerRadius"; + +static const NSTextAlignment BITTextAlignmentLeft = NSTextAlignmentLeft; +static const NSTextAlignment BITTextAlignmentCenter = NSTextAlignmentCenter; +static const NSTextAlignment BITTextAlignmentRight = NSTextAlignmentRight; +static const NSTextAlignment BITTextAlignmentJustified = NSTextAlignmentJustified; +static const NSTextAlignment BITTextAlignmentNatural = NSTextAlignmentNatural; + +static const NSLineBreakMode BITLineBreakByWordWrapping = NSLineBreakByWordWrapping; +static const NSLineBreakMode BITLineBreakByCharWrapping = NSLineBreakByCharWrapping; +static const NSLineBreakMode BITLineBreakByClipping = NSLineBreakByClipping; +static const NSLineBreakMode BITLineBreakByTruncatingHead = NSLineBreakByTruncatingHead; +static const NSLineBreakMode BITLineBreakByTruncatingMiddle = NSLineBreakByTruncatingMiddle; +static const NSLineBreakMode BITLineBreakByTruncatingTail = NSLineBreakByTruncatingTail; + +typedef NSTextAlignment BITTextAlignment; +typedef NSLineBreakMode BITLineBreakMode; + + +static inline CGFLOAT_TYPE CGFloat_ceil(CGFLOAT_TYPE cgfloat) { +#if CGFLOAT_IS_DOUBLE + return ceil(cgfloat); +#else + return ceilf(cgfloat); +#endif +} + +static inline CGFLOAT_TYPE CGFloat_floor(CGFLOAT_TYPE cgfloat) { +#if CGFLOAT_IS_DOUBLE + return floor(cgfloat); +#else + return floorf(cgfloat); +#endif +} + +static inline CGFLOAT_TYPE CGFloat_round(CGFLOAT_TYPE cgfloat) { +#if CGFLOAT_IS_DOUBLE + return round(cgfloat); +#else + return roundf(cgfloat); +#endif +} + +static inline CGFLOAT_TYPE CGFloat_sqrt(CGFLOAT_TYPE cgfloat) { +#if CGFLOAT_IS_DOUBLE + return sqrt(cgfloat); +#else + return sqrtf(cgfloat); +#endif +} + +static inline CGFloat BITFlushFactorForTextAlignment(NSTextAlignment textAlignment) { + switch (textAlignment) { + case BITTextAlignmentCenter: + return 0.5f; + case BITTextAlignmentRight: + return 1.0f; + case BITTextAlignmentLeft: + default: + return 0.0f; + } +} + +static inline NSDictionary * NSAttributedStringAttributesFromLabel(BITAttributedLabel *label) { + NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionary]; + + [mutableAttributes setObject:label.font forKey:(NSString *)kCTFontAttributeName]; + [mutableAttributes setObject:label.textColor forKey:(NSString *)kCTForegroundColorAttributeName]; + [mutableAttributes setObject:@(label.kern) forKey:(NSString *)kCTKernAttributeName]; + + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.alignment = label.textAlignment; + paragraphStyle.lineSpacing = label.lineSpacing; + paragraphStyle.minimumLineHeight = label.minimumLineHeight > 0 ? label.minimumLineHeight : label.font.lineHeight * label.lineHeightMultiple; + paragraphStyle.maximumLineHeight = label.maximumLineHeight > 0 ? label.maximumLineHeight : label.font.lineHeight * label.lineHeightMultiple; + paragraphStyle.lineHeightMultiple = label.lineHeightMultiple; + paragraphStyle.firstLineHeadIndent = label.firstLineIndent; + + if (label.numberOfLines == 1) { + paragraphStyle.lineBreakMode = label.lineBreakMode; + } else { + paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; + } + + [mutableAttributes setObject:paragraphStyle forKey:(NSString *)kCTParagraphStyleAttributeName]; + + return [NSDictionary dictionaryWithDictionary:mutableAttributes]; +} + +static inline CGColorRef CGColorRefFromColor(id color); +static inline NSDictionary * convertNSAttributedStringAttributesToCTAttributes(NSDictionary *attributes); + +static inline NSAttributedString * NSAttributedStringByScalingFontSize(NSAttributedString *attributedString, CGFloat scale) { + NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy]; + [mutableAttributedString enumerateAttribute:(NSString *)kCTFontAttributeName inRange:NSMakeRange(0, [mutableAttributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL * __unused stop) { + UIFont *font = (UIFont *)value; + if (font) { + NSString *fontName; + CGFloat pointSize; + + if ([font isKindOfClass:[UIFont class]]) { + fontName = font.fontName; + pointSize = font.pointSize; + } else { + fontName = (NSString *)CFBridgingRelease(CTFontCopyName((__bridge CTFontRef)font, kCTFontPostScriptNameKey)); + pointSize = CTFontGetSize((__bridge CTFontRef)font); + } + + [mutableAttributedString removeAttribute:(NSString *)kCTFontAttributeName range:range]; + CTFontRef fontRef = CTFontCreateWithName((__bridge CFStringRef)fontName, CGFloat_floor(pointSize * scale), NULL); + [mutableAttributedString addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id)fontRef range:range]; + CFRelease(fontRef); + } + }]; + + return mutableAttributedString; +} + +static inline NSAttributedString * NSAttributedStringBySettingColorFromContext(NSAttributedString *attributedString, UIColor *color) { + if (!color) { + return attributedString; + } + + NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy]; + [mutableAttributedString enumerateAttribute:(NSString *)kCTForegroundColorFromContextAttributeName inRange:NSMakeRange(0, [mutableAttributedString length]) options:0 usingBlock:^(id value, NSRange range, __unused BOOL *stop) { + BOOL usesColorFromContext = (BOOL)value; + if (usesColorFromContext) { + [mutableAttributedString setAttributes:[NSDictionary dictionaryWithObject:color forKey:(NSString *)kCTForegroundColorAttributeName] range:range]; + [mutableAttributedString removeAttribute:(NSString *)kCTForegroundColorFromContextAttributeName range:range]; + } + }]; + + return mutableAttributedString; +} + +static inline CGSize CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(CTFramesetterRef framesetter, NSAttributedString *attributedString, CGSize size, NSUInteger numberOfLines) { + CFRange rangeToSize = CFRangeMake(0, (CFIndex)[attributedString length]); + CGSize constraints = CGSizeMake(size.width, BITFLOAT_MAX); + + if (numberOfLines == 1) { + // If there is one line, the size that fits is the full width of the line + constraints = CGSizeMake(BITFLOAT_MAX, BITFLOAT_MAX); + } else if (numberOfLines > 0) { + // If the line count of the label more than 1, limit the range to size to the number of lines that have been set + CGMutablePathRef path = CGPathCreateMutable(); + CGPathAddRect(path, NULL, CGRectMake(0.0f, 0.0f, constraints.width, BITFLOAT_MAX)); + CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL); + CFArrayRef lines = CTFrameGetLines(frame); + + if (CFArrayGetCount(lines) > 0) { + NSInteger lastVisibleLineIndex = MIN((CFIndex)numberOfLines, CFArrayGetCount(lines)) - 1; + CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex); + + CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine); + rangeToSize = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length); + } + + CFRelease(frame); + CGPathRelease(path); + } + + CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, rangeToSize, NULL, constraints, NULL); + + return CGSizeMake(CGFloat_ceil(suggestedSize.width), CGFloat_ceil(suggestedSize.height)); +} + +@interface BITAccessibilityElement : UIAccessibilityElement +@property (nonatomic, weak) UIView *superview; +@property (nonatomic, assign) CGRect boundingRect; +@end + +@implementation BITAccessibilityElement + +- (CGRect)accessibilityFrame { + return UIAccessibilityConvertFrameToScreenCoordinates(self.boundingRect, self.superview); +} + +@end + +@interface BITAttributedLabel () +@property (readwrite, nonatomic, copy) NSAttributedString *inactiveAttributedText; +@property (readwrite, nonatomic, copy) NSAttributedString *renderedAttributedText; +@property (readwrite, atomic, strong) NSDataDetector *dataDetector; +@property (readwrite, nonatomic, strong) NSArray *linkModels; +@property (readwrite, nonatomic, strong) BITAttributedLabelLink *activeLink; +@property (readwrite, nonatomic, strong) NSArray *accessibilityElements; + +- (void) longPressGestureDidFire:(UILongPressGestureRecognizer *)sender; +@end + +@implementation BITAttributedLabel { +@private + BOOL _needsFramesetter; + CTFramesetterRef _framesetter; + CTFramesetterRef _highlightFramesetter; +} + +@dynamic text; +@synthesize attributedText = _attributedText; + +#ifndef kCFCoreFoundationVersionNumber_iOS_7_0 +#define kCFCoreFoundationVersionNumber_iOS_7_0 847.2 +#endif + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0) { + Class class = [self class]; + Class superclass = class_getSuperclass(class); + + NSArray *strings = @[ + NSStringFromSelector(@selector(isAccessibilityElement)), + NSStringFromSelector(@selector(accessibilityElementCount)), + NSStringFromSelector(@selector(accessibilityElementAtIndex:)), + NSStringFromSelector(@selector(indexOfAccessibilityElement:)), + ]; + + for (NSString *string in strings) { + SEL selector = NSSelectorFromString(string); + IMP superImplementation = class_getMethodImplementation(superclass, selector); + Method method = class_getInstanceMethod(class, selector); + const char *types = method_getTypeEncoding(method); + class_replaceMethod(class, selector, superImplementation, types); + } + } + }); +} + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (!self) { + return nil; + } + + [self commonInit]; + + return self; +} + +- (void)commonInit { + self.userInteractionEnabled = YES; +#if !TARGET_OS_TV + self.multipleTouchEnabled = NO; +#endif + + self.textInsets = UIEdgeInsetsZero; + self.lineHeightMultiple = 1.0f; + + self.linkModels = [NSArray array]; + + self.linkBackgroundEdgeInset = UIEdgeInsetsMake(0.0f, -1.0f, 0.0f, -1.0f); + + NSMutableDictionary *mutableLinkAttributes = [NSMutableDictionary dictionary]; + [mutableLinkAttributes setObject:[NSNumber numberWithBool:YES] forKey:(NSString *)kCTUnderlineStyleAttributeName]; + + NSMutableDictionary *mutableActiveLinkAttributes = [NSMutableDictionary dictionary]; + [mutableActiveLinkAttributes setObject:[NSNumber numberWithBool:NO] forKey:(NSString *)kCTUnderlineStyleAttributeName]; + + NSMutableDictionary *mutableInactiveLinkAttributes = [NSMutableDictionary dictionary]; + [mutableInactiveLinkAttributes setObject:[NSNumber numberWithBool:NO] forKey:(NSString *)kCTUnderlineStyleAttributeName]; + + if ([NSMutableParagraphStyle class]) { + [mutableLinkAttributes setObject:[UIColor blueColor] forKey:(NSString *)kCTForegroundColorAttributeName]; + [mutableActiveLinkAttributes setObject:[UIColor redColor] forKey:(NSString *)kCTForegroundColorAttributeName]; + [mutableInactiveLinkAttributes setObject:[UIColor grayColor] forKey:(NSString *)kCTForegroundColorAttributeName]; + } else { + [mutableLinkAttributes setObject:(__bridge id)[[UIColor blueColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName]; + [mutableActiveLinkAttributes setObject:(__bridge id)[[UIColor redColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName]; + [mutableInactiveLinkAttributes setObject:(__bridge id)[[UIColor grayColor] CGColor] forKey:(NSString *)kCTForegroundColorAttributeName]; + } + + self.linkAttributes = [NSDictionary dictionaryWithDictionary:mutableLinkAttributes]; + self.activeLinkAttributes = [NSDictionary dictionaryWithDictionary:mutableActiveLinkAttributes]; + self.inactiveLinkAttributes = [NSDictionary dictionaryWithDictionary:mutableInactiveLinkAttributes]; + _extendsLinkTouchArea = NO; + _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self + action:@selector(longPressGestureDidFire:)]; + self.longPressGestureRecognizer.delegate = self; + [self addGestureRecognizer:self.longPressGestureRecognizer]; +} + +- (void)dealloc { + if (_framesetter) { + CFRelease(_framesetter); + } + + if (_highlightFramesetter) { + CFRelease(_highlightFramesetter); + } + + if (_longPressGestureRecognizer) { + [self removeGestureRecognizer:_longPressGestureRecognizer]; + } +} + +#pragma mark - + ++ (CGSize)sizeThatFitsAttributedString:(NSAttributedString *)attributedString + withConstraints:(CGSize)size + limitedToNumberOfLines:(NSUInteger)numberOfLines +{ + if (!attributedString || attributedString.length == 0) { + return CGSizeZero; + } + + CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attributedString); + + CGSize calculatedSize = CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(framesetter, attributedString, size, numberOfLines); + + CFRelease(framesetter); + + return calculatedSize; +} + +#pragma mark - + +- (void)setAttributedText:(NSAttributedString *)text { + if ([text isEqualToAttributedString:_attributedText]) { + return; + } + + _attributedText = [text copy]; + + [self setNeedsFramesetter]; + [self setNeedsDisplay]; + + if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) { + [self invalidateIntrinsicContentSize]; + } + + [super setText:[self.attributedText string]]; +} + +- (NSAttributedString *)renderedAttributedText { + if (!_renderedAttributedText) { + NSMutableAttributedString *fullString = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText]; + + if (self.attributedTruncationToken) { + [fullString appendAttributedString:self.attributedTruncationToken]; + } + + NSAttributedString *string = [[NSAttributedString alloc] initWithAttributedString:fullString]; + self.renderedAttributedText = NSAttributedStringBySettingColorFromContext(string, self.textColor); + } + + return _renderedAttributedText; +} + +- (NSArray *) links { + return [_linkModels valueForKey:@"result"]; +} + +- (void)setLinkModels:(NSArray *)linkModels { + _linkModels = linkModels; + + self.accessibilityElements = nil; +} + +- (void)setNeedsFramesetter { + // Reset the rendered attributed text so it has a chance to regenerate + self.renderedAttributedText = nil; + + _needsFramesetter = YES; +} + +- (CTFramesetterRef)framesetter { + if (_needsFramesetter) { + @synchronized(self) { + CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self.renderedAttributedText); + [self setFramesetter:framesetter]; + [self setHighlightFramesetter:nil]; + _needsFramesetter = NO; + + if (framesetter) { + CFRelease(framesetter); + } + } + } + + return _framesetter; +} + +- (void)setFramesetter:(CTFramesetterRef)framesetter { + if (framesetter) { + CFRetain(framesetter); + } + + if (_framesetter) { + CFRelease(_framesetter); + } + + _framesetter = framesetter; +} + +- (CTFramesetterRef)highlightFramesetter { + return _highlightFramesetter; +} + +- (void)setHighlightFramesetter:(CTFramesetterRef)highlightFramesetter { + if (highlightFramesetter) { + CFRetain(highlightFramesetter); + } + + if (_highlightFramesetter) { + CFRelease(_highlightFramesetter); + } + + _highlightFramesetter = highlightFramesetter; +} + +#pragma mark - + +- (void)setEnabledTextCheckingTypes:(NSTextCheckingTypes)enabledTextCheckingTypes { + if (self.enabledTextCheckingTypes == enabledTextCheckingTypes) { + return; + } + + _enabledTextCheckingTypes = enabledTextCheckingTypes; + + // one detector instance per type (combination), fast reuse e.g. in cells + static NSMutableDictionary *dataDetectorsByType = nil; + + if (!dataDetectorsByType) { + dataDetectorsByType = [NSMutableDictionary dictionary]; + } + + if (enabledTextCheckingTypes) { + if (![dataDetectorsByType objectForKey:@(enabledTextCheckingTypes)]) { + NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:enabledTextCheckingTypes + error:nil]; + if (detector) { + [dataDetectorsByType setObject:detector forKey:@(enabledTextCheckingTypes)]; + } + } + self.dataDetector = [dataDetectorsByType objectForKey:@(enabledTextCheckingTypes)]; + } else { + self.dataDetector = nil; + } +} + +- (void)addLink:(BITAttributedLabelLink *)link { + [self addLinks:@[link]]; +} + +- (void)addLinks:(NSArray *)links { + NSMutableArray *mutableLinkModels = [NSMutableArray arrayWithArray:self.linkModels]; + + NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy]; + + for (BITAttributedLabelLink *link in links) { + if (link.attributes) { + [mutableAttributedString addAttributes:link.attributes range:link.result.range]; + } + } + + self.attributedText = mutableAttributedString; + [self setNeedsDisplay]; + + [mutableLinkModels addObjectsFromArray:links]; + + self.linkModels = [NSArray arrayWithArray:mutableLinkModels]; +} + +- (BITAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result + attributes:(NSDictionary *)attributes +{ + return [self addLinksWithTextCheckingResults:@[result] attributes:attributes].firstObject; +} + +- (NSArray *)addLinksWithTextCheckingResults:(NSArray *)results + attributes:(NSDictionary *)attributes +{ + NSMutableArray *links = [NSMutableArray array]; + + for (NSTextCheckingResult *result in results) { + NSDictionary *activeAttributes = attributes ? self.activeLinkAttributes : nil; + NSDictionary *inactiveAttributes = attributes ? self.inactiveLinkAttributes : nil; + + BITAttributedLabelLink *link = [[BITAttributedLabelLink alloc] initWithAttributes:attributes + activeAttributes:activeAttributes + inactiveAttributes:inactiveAttributes + textCheckingResult:result]; + + [links addObject:link]; + } + + [self addLinks:links]; + + return links; +} + +- (BITAttributedLabelLink *)addLinkWithTextCheckingResult:(NSTextCheckingResult *)result { + return [self addLinkWithTextCheckingResult:result attributes:self.linkAttributes]; +} + +- (BITAttributedLabelLink *)addLinkToURL:(NSURL *)url + withRange:(NSRange)range +{ + return [self addLinkWithTextCheckingResult:[NSTextCheckingResult linkCheckingResultWithRange:range URL:url]]; +} + +- (BITAttributedLabelLink *)addLinkToAddress:(NSDictionary *)addressComponents + withRange:(NSRange)range +{ + return [self addLinkWithTextCheckingResult:[NSTextCheckingResult addressCheckingResultWithRange:range components:addressComponents]]; +} + +- (BITAttributedLabelLink *)addLinkToPhoneNumber:(NSString *)phoneNumber + withRange:(NSRange)range +{ + return [self addLinkWithTextCheckingResult:[NSTextCheckingResult phoneNumberCheckingResultWithRange:range phoneNumber:phoneNumber]]; +} + +- (BITAttributedLabelLink *)addLinkToDate:(NSDate *)date + withRange:(NSRange)range +{ + return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date]]; +} + +- (BITAttributedLabelLink *)addLinkToDate:(NSDate *)date + timeZone:(NSTimeZone *)timeZone + duration:(NSTimeInterval)duration + withRange:(NSRange)range +{ + return [self addLinkWithTextCheckingResult:[NSTextCheckingResult dateCheckingResultWithRange:range date:date timeZone:timeZone duration:duration]]; +} + +- (BITAttributedLabelLink *)addLinkToTransitInformation:(NSDictionary *)components + withRange:(NSRange)range +{ + return [self addLinkWithTextCheckingResult:[NSTextCheckingResult transitInformationCheckingResultWithRange:range components:components]]; +} + +#pragma mark - + +- (BOOL)containslinkAtPoint:(CGPoint)point { + return [self linkAtPoint:point] != nil; +} + +- (BITAttributedLabelLink *)linkAtPoint:(CGPoint)point { + + // Stop quickly if none of the points to be tested are in the bounds. + if (!CGRectContainsPoint(CGRectInset(self.bounds, -15.f, -15.f), point) || self.links.count == 0) { + return nil; + } + + BITAttributedLabelLink *result = [self linkAtCharacterIndex:[self characterIndexAtPoint:point]]; + + if (!result && self.extendsLinkTouchArea) { + result = [self linkAtRadius:2.5f aroundPoint:point] + ?: [self linkAtRadius:5.f aroundPoint:point] + ?: [self linkAtRadius:7.5f aroundPoint:point] + ?: [self linkAtRadius:12.5f aroundPoint:point] + ?: [self linkAtRadius:15.f aroundPoint:point]; + } + + return result; +} + +- (BITAttributedLabelLink *)linkAtRadius:(const CGFloat)radius aroundPoint:(CGPoint)point { + const CGFloat diagonal = CGFloat_sqrt(2 * radius * radius); + const CGPoint deltas[] = { + CGPointMake(0, -radius), CGPointMake(0, radius), // Above and below + CGPointMake(-radius, 0), CGPointMake(radius, 0), // Beside + CGPointMake(-diagonal, -diagonal), CGPointMake(-diagonal, diagonal), + CGPointMake(diagonal, diagonal), CGPointMake(diagonal, -diagonal) // Diagonal + }; + const size_t count = sizeof(deltas) / sizeof(CGPoint); + + BITAttributedLabelLink *link = nil; + + for (NSUInteger i = 0; i < count && link.result == nil; i ++) { + CGPoint currentPoint = CGPointMake(point.x + deltas[i].x, point.y + deltas[i].y); + link = [self linkAtCharacterIndex:[self characterIndexAtPoint:currentPoint]]; + } + + return link; +} + +- (BITAttributedLabelLink *)linkAtCharacterIndex:(CFIndex)idx { + // Do not enumerate if the index is outside of the bounds of the text. + if (!NSLocationInRange((NSUInteger)idx, NSMakeRange(0, self.attributedText.length))) { + return nil; + } + + NSEnumerator *enumerator = [self.linkModels reverseObjectEnumerator]; + BITAttributedLabelLink *link = nil; + while ((link = [enumerator nextObject])) { + if (NSLocationInRange((NSUInteger)idx, link.result.range)) { + return link; + } + } + + return nil; +} + +- (CFIndex)characterIndexAtPoint:(CGPoint)p { + if (!CGRectContainsPoint(self.bounds, p)) { + return NSNotFound; + } + + CGRect textRect = [self textRectForBounds:self.bounds limitedToNumberOfLines:self.numberOfLines]; + if (!CGRectContainsPoint(textRect, p)) { + return NSNotFound; + } + + // Offset tap coordinates by textRect origin to make them relative to the origin of frame + p = CGPointMake(p.x - textRect.origin.x, p.y - textRect.origin.y); + // Convert tap coordinates (start at top left) to CT coordinates (start at bottom left) + p = CGPointMake(p.x, textRect.size.height - p.y); + + CGMutablePathRef path = CGPathCreateMutable(); + CGPathAddRect(path, NULL, textRect); + CTFrameRef frame = CTFramesetterCreateFrame([self framesetter], CFRangeMake(0, (CFIndex)[self.attributedText length]), path, NULL); + if (frame == NULL) { + CGPathRelease(path); + return NSNotFound; + } + + CFArrayRef lines = CTFrameGetLines(frame); + NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines); + if (numberOfLines == 0) { + CFRelease(frame); + CGPathRelease(path); + return NSNotFound; + } + + CFIndex idx = NSNotFound; + + CGPoint lineOrigins[numberOfLines]; + CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins); + + for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) { + CGPoint lineOrigin = lineOrigins[lineIndex]; + CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex); + + // Get bounding information of line + CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f; + CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading); + CGFloat yMin = (CGFloat)floor(lineOrigin.y - descent); + CGFloat yMax = (CGFloat)ceil(lineOrigin.y + ascent); + + // Apply penOffset using flushFactor for horizontal alignment to set lineOrigin since this is the horizontal offset from drawFramesetter + CGFloat flushFactor = BITFlushFactorForTextAlignment(self.textAlignment); + CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, textRect.size.width); + lineOrigin.x = penOffset; + + // Check if we've already passed the line + if (p.y > yMax) { + break; + } + // Check if the point is within this line vertically + if (p.y >= yMin) { + // Check if the point is within this line horizontally + if (p.x >= lineOrigin.x && p.x <= lineOrigin.x + width) { + // Convert CT coordinates to line-relative coordinates + CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y); + idx = CTLineGetStringIndexForPosition(line, relativePoint); + break; + } + } + } + + CFRelease(frame); + CGPathRelease(path); + + return idx; +} + +- (CGRect)boundingRectForCharacterRange:(NSRange)range { + NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy]; + + NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:mutableAttributedString]; + + NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; + [textStorage addLayoutManager:layoutManager]; + + NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.bounds.size]; + [layoutManager addTextContainer:textContainer]; + + NSRange glyphRange; + [layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange]; + + return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer]; +} + +- (void)drawFramesetter:(CTFramesetterRef)framesetter + attributedString:(NSAttributedString *)attributedString + textRange:(CFRange)textRange + inRect:(CGRect)rect + context:(CGContextRef)c +{ + CGMutablePathRef path = CGPathCreateMutable(); + CGPathAddRect(path, NULL, rect); + CTFrameRef frame = CTFramesetterCreateFrame(framesetter, textRange, path, NULL); + + [self drawBackground:frame inRect:rect context:c]; + + CFArrayRef lines = CTFrameGetLines(frame); + NSInteger numberOfLines = self.numberOfLines > 0 ? MIN(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines); + BOOL truncateLastLine = (self.lineBreakMode == BITLineBreakByTruncatingHead || self.lineBreakMode == BITLineBreakByTruncatingMiddle || self.lineBreakMode == BITLineBreakByTruncatingTail); + + CGPoint lineOrigins[numberOfLines]; + CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins); + + for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) { + CGPoint lineOrigin = lineOrigins[lineIndex]; + CGContextSetTextPosition(c, lineOrigin.x, lineOrigin.y); + CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex); + + CGFloat descent = 0.0f; + CTLineGetTypographicBounds((CTLineRef)line, NULL, &descent, NULL); + + // Adjust pen offset for flush depending on text alignment + CGFloat flushFactor = BITFlushFactorForTextAlignment(self.textAlignment); + + if (lineIndex == numberOfLines - 1 && truncateLastLine) { + // Check if the range of text in the last line reaches the end of the full attributed string + CFRange lastLineRange = CTLineGetStringRange(line); + + if (!(lastLineRange.length == 0 && lastLineRange.location == 0) && lastLineRange.location + lastLineRange.length < textRange.location + textRange.length) { + // Get correct truncationType and attribute position + CTLineTruncationType truncationType; + CFIndex truncationAttributePosition = lastLineRange.location; + BITLineBreakMode lineBreakMode = self.lineBreakMode; + + // Multiple lines, only use UILineBreakModeTailTruncation + if (numberOfLines != 1) { + lineBreakMode = BITLineBreakByTruncatingTail; + } + + switch (lineBreakMode) { + case BITLineBreakByTruncatingHead: + truncationType = kCTLineTruncationStart; + break; + case BITLineBreakByTruncatingMiddle: + truncationType = kCTLineTruncationMiddle; + truncationAttributePosition += (lastLineRange.length / 2); + break; + case BITLineBreakByTruncatingTail: + default: + truncationType = kCTLineTruncationEnd; + truncationAttributePosition += (lastLineRange.length - 1); + break; + } + + NSAttributedString *attributedTruncationString = self.attributedTruncationToken; + if (!attributedTruncationString) { + NSString *truncationTokenString = @"\u2026"; // Unicode Character 'HORIZONTAL ELLIPSIS' (U+2026) + + NSDictionary *truncationTokenStringAttributes = truncationTokenStringAttributes = [attributedString attributesAtIndex:(NSUInteger)truncationAttributePosition effectiveRange:NULL]; + + attributedTruncationString = [[NSAttributedString alloc] initWithString:truncationTokenString attributes:truncationTokenStringAttributes]; + } + CTLineRef truncationToken = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attributedTruncationString); + + // Append truncationToken to the string + // because if string isn't too long, CT won't add the truncationToken on its own. + // There is no chance of a double truncationToken because CT only adds the + // token if it removes characters (and the one we add will go first) + NSMutableAttributedString *truncationString = [[NSMutableAttributedString alloc] initWithAttributedString: + [attributedString attributedSubstringFromRange: + NSMakeRange((NSUInteger)lastLineRange.location, + (NSUInteger)lastLineRange.length)]]; + if (lastLineRange.length > 0) { + // Remove any newline at the end (we don't want newline space between the text and the truncation token). There can only be one, because the second would be on the next line. + unichar lastCharacter = [[truncationString string] characterAtIndex:(NSUInteger)(lastLineRange.length - 1)]; + if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) { + [truncationString deleteCharactersInRange:NSMakeRange((NSUInteger)(lastLineRange.length - 1), 1)]; + } + } + [truncationString appendAttributedString:attributedTruncationString]; + CTLineRef truncationLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)truncationString); + + // Truncate the line in case it is too long. + CTLineRef truncatedLine = CTLineCreateTruncatedLine(truncationLine, rect.size.width, truncationType, truncationToken); + if (!truncatedLine) { + // If the line is not as wide as the truncationToken, truncatedLine is NULL + truncatedLine = CFRetain(truncationToken); + } + + CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(truncatedLine, flushFactor, rect.size.width); + CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender); + + CTLineDraw(truncatedLine, c); + + NSRange linkRange; + if ([attributedTruncationString attribute:NSLinkAttributeName atIndex:0 effectiveRange:&linkRange]) { + NSRange tokenRange = [truncationString.string rangeOfString:attributedTruncationString.string]; + NSRange tokenLinkRange = NSMakeRange((NSUInteger)(lastLineRange.location+lastLineRange.length)-tokenRange.length, (NSUInteger)tokenRange.length); + + [self addLinkToURL:[attributedTruncationString attribute:NSLinkAttributeName atIndex:0 effectiveRange:&linkRange] withRange:tokenLinkRange]; + } + + CFRelease(truncatedLine); + CFRelease(truncationLine); + CFRelease(truncationToken); + } else { + CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width); + CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender); + CTLineDraw(line, c); + } + } else { + CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width); + CGContextSetTextPosition(c, penOffset, lineOrigin.y - descent - self.font.descender); + CTLineDraw(line, c); + } + } + + [self drawStrike:frame inRect:rect context:c]; + + CFRelease(frame); + CGPathRelease(path); +} + +- (void)drawBackground:(CTFrameRef)frame + inRect:(CGRect)rect + context:(CGContextRef)c +{ + NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame); + CGPoint origins[[lines count]]; + CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins); + + CFIndex lineIndex = 0; + for (id line in lines) { + CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f; + CGFloat width = (CGFloat)CTLineGetTypographicBounds((__bridge CTLineRef)line, &ascent, &descent, &leading) ; + + for (id glyphRun in (__bridge NSArray *)CTLineGetGlyphRuns((__bridge CTLineRef)line)) { + NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes((__bridge CTRunRef) glyphRun); + CGColorRef strokeColor = CGColorRefFromColor([attributes objectForKey:kBITBackgroundStrokeColorAttributeName]); + CGColorRef fillColor = CGColorRefFromColor([attributes objectForKey:kBITBackgroundFillColorAttributeName]); + UIEdgeInsets fillPadding = [[attributes objectForKey:kBITBackgroundFillPaddingAttributeName] UIEdgeInsetsValue]; + CGFloat cornerRadius = [[attributes objectForKey:kBITBackgroundCornerRadiusAttributeName] floatValue]; + CGFloat lineWidth = [[attributes objectForKey:kBITBackgroundLineWidthAttributeName] floatValue]; + + if (strokeColor || fillColor) { + CGRect runBounds = CGRectZero; + CGFloat runAscent = 0.0f; + CGFloat runDescent = 0.0f; + + runBounds.size.width = (CGFloat)CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &runAscent, &runDescent, NULL) + fillPadding.left + fillPadding.right; + runBounds.size.height = runAscent + runDescent + fillPadding.top + fillPadding.bottom; + + CGFloat xOffset = 0.0f; + CFRange glyphRange = CTRunGetStringRange((__bridge CTRunRef)glyphRun); + switch (CTRunGetStatus((__bridge CTRunRef)glyphRun)) { + case kCTRunStatusRightToLeft: + xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location + glyphRange.length, NULL); + break; + default: + xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location, NULL); + break; + } + + runBounds.origin.x = origins[lineIndex].x + rect.origin.x + xOffset - fillPadding.left - rect.origin.x; + runBounds.origin.y = origins[lineIndex].y + rect.origin.y - fillPadding.bottom - rect.origin.y; + runBounds.origin.y -= runDescent; + + // Don't draw higlightedLinkBackground too far to the right + if (CGRectGetWidth(runBounds) > width) { + runBounds.size.width = width; + } + + CGPathRef path = [[UIBezierPath bezierPathWithRoundedRect:CGRectInset(UIEdgeInsetsInsetRect(runBounds, self.linkBackgroundEdgeInset), lineWidth, lineWidth) cornerRadius:cornerRadius] CGPath]; + + CGContextSetLineJoin(c, kCGLineJoinRound); + + if (fillColor) { + CGContextSetFillColorWithColor(c, fillColor); + CGContextAddPath(c, path); + CGContextFillPath(c); + } + + if (strokeColor) { + CGContextSetStrokeColorWithColor(c, strokeColor); + CGContextAddPath(c, path); + CGContextStrokePath(c); + } + } + } + + lineIndex++; + } +} + +- (void)drawStrike:(CTFrameRef)frame + inRect:(__unused CGRect)rect + context:(CGContextRef)c +{ + NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame); + CGPoint origins[[lines count]]; + CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins); + + CFIndex lineIndex = 0; + for (id line in lines) { + CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f; + CGFloat width = (CGFloat)CTLineGetTypographicBounds((__bridge CTLineRef)line, &ascent, &descent, &leading) ; + + for (id glyphRun in (__bridge NSArray *)CTLineGetGlyphRuns((__bridge CTLineRef)line)) { + NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes((__bridge CTRunRef) glyphRun); + BOOL strikeOut = [[attributes objectForKey:kBITStrikeOutAttributeName] boolValue]; + NSInteger superscriptStyle = [[attributes objectForKey:(id)kCTSuperscriptAttributeName] integerValue]; + + if (strikeOut) { + CGRect runBounds = CGRectZero; + CGFloat runAscent = 0.0f; + CGFloat runDescent = 0.0f; + + runBounds.size.width = (CGFloat)CTRunGetTypographicBounds((__bridge CTRunRef)glyphRun, CFRangeMake(0, 0), &runAscent, &runDescent, NULL); + runBounds.size.height = runAscent + runDescent; + + CGFloat xOffset = 0.0f; + CFRange glyphRange = CTRunGetStringRange((__bridge CTRunRef)glyphRun); + switch (CTRunGetStatus((__bridge CTRunRef)glyphRun)) { + case kCTRunStatusRightToLeft: + xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location + glyphRange.length, NULL); + break; + default: + xOffset = CTLineGetOffsetForStringIndex((__bridge CTLineRef)line, glyphRange.location, NULL); + break; + } + runBounds.origin.x = origins[lineIndex].x + xOffset; + runBounds.origin.y = origins[lineIndex].y; + runBounds.origin.y -= runDescent; + + // Don't draw strikeout too far to the right + if (CGRectGetWidth(runBounds) > width) { + runBounds.size.width = width; + } + + switch (superscriptStyle) { + case 1: + runBounds.origin.y -= runAscent * 0.47f; + break; + case -1: + runBounds.origin.y += runAscent * 0.25f; + break; + default: + break; + } + + // Use text color, or default to black + id color = [attributes objectForKey:(id)kCTForegroundColorAttributeName]; + if (color) { + CGContextSetStrokeColorWithColor(c, CGColorRefFromColor(color)); + } else { + CGContextSetGrayStrokeColor(c, 0.0f, 1.0); + } + + CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)self.font.fontName, self.font.pointSize, NULL); + CGContextSetLineWidth(c, CTFontGetUnderlineThickness(font)); + CFRelease(font); + + CGFloat y = CGFloat_round(runBounds.origin.y + runBounds.size.height / 2.0f); + CGContextMoveToPoint(c, runBounds.origin.x, y); + CGContextAddLineToPoint(c, runBounds.origin.x + runBounds.size.width, y); + + CGContextStrokePath(c); + } + } + + lineIndex++; + } +} + +#pragma mark - BITAttributedLabel + +- (void)setText:(id)text { + NSParameterAssert(!text || [text isKindOfClass:[NSAttributedString class]] || [text isKindOfClass:[NSString class]]); + + if ([text isKindOfClass:[NSString class]]) { + [self setText:text afterInheritingLabelAttributesAndConfiguringWithBlock:nil]; + return; + } + + self.attributedText = text; + self.activeLink = nil; + + self.linkModels = [NSArray array]; + if (text && self.attributedText && self.enabledTextCheckingTypes) { + __weak __typeof(self)weakSelf = self; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + __strong __typeof(weakSelf)strongSelf = weakSelf; + + NSDataDetector *dataDetector = strongSelf.dataDetector; + if (dataDetector && [dataDetector respondsToSelector:@selector(matchesInString:options:range:)]) { + NSArray *results = [dataDetector matchesInString:[(NSAttributedString *)text string] options:0 range:NSMakeRange(0, [(NSAttributedString *)text length])]; + if ([results count] > 0) { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[strongSelf.attributedText string] isEqualToString:[(NSAttributedString *)text string]]) { + [strongSelf addLinksWithTextCheckingResults:results attributes:strongSelf.linkAttributes]; + } + }); + } + } + }); + } + + [self.attributedText enumerateAttribute:NSLinkAttributeName inRange:NSMakeRange(0, self.attributedText.length) options:0 usingBlock:^(id value, NSRange range, __unused BOOL *stop) { + if (value) { + NSURL *URL = [value isKindOfClass:[NSString class]] ? [NSURL URLWithString:value] : value; + [self addLinkToURL:URL withRange:range]; + } + }]; +} + +- (void)setText:(id)text +afterInheritingLabelAttributesAndConfiguringWithBlock:(NSMutableAttributedString * (^)(NSMutableAttributedString *mutableAttributedString))block +{ + NSMutableAttributedString *mutableAttributedString = nil; + if ([text isKindOfClass:[NSString class]]) { + mutableAttributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:NSAttributedStringAttributesFromLabel(self)]; + } else { + mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:text]; + [mutableAttributedString addAttributes:NSAttributedStringAttributesFromLabel(self) range:NSMakeRange(0, [mutableAttributedString length])]; + } + + if (block) { + mutableAttributedString = block(mutableAttributedString); + } + + [self setText:mutableAttributedString]; +} + +- (void)setActiveLink:(BITAttributedLabelLink *)activeLink { + _activeLink = activeLink; + + NSDictionary *activeAttributes = activeLink.activeAttributes ?: self.activeLinkAttributes; + + if (_activeLink && activeAttributes.count > 0) { + if (!self.inactiveAttributedText) { + self.inactiveAttributedText = [self.attributedText copy]; + } + + NSMutableAttributedString *mutableAttributedString = [self.inactiveAttributedText mutableCopy]; + if (self.activeLink.result.range.length > 0 && NSLocationInRange(NSMaxRange(self.activeLink.result.range) - 1, NSMakeRange(0, [self.inactiveAttributedText length]))) { + [mutableAttributedString addAttributes:activeAttributes range:self.activeLink.result.range]; + } + + self.attributedText = mutableAttributedString; + [self setNeedsDisplay]; + + [CATransaction flush]; + } else if (self.inactiveAttributedText) { + self.attributedText = self.inactiveAttributedText; + self.inactiveAttributedText = nil; + + [self setNeedsDisplay]; + } +} + +- (void)setLinkAttributes:(NSDictionary *)linkAttributes { + _linkAttributes = convertNSAttributedStringAttributesToCTAttributes(linkAttributes); +} + +- (void)setActiveLinkAttributes:(NSDictionary *)activeLinkAttributes { + _activeLinkAttributes = convertNSAttributedStringAttributesToCTAttributes(activeLinkAttributes); +} + +- (void)setInactiveLinkAttributes:(NSDictionary *)inactiveLinkAttributes { + _inactiveLinkAttributes = convertNSAttributedStringAttributesToCTAttributes(inactiveLinkAttributes); +} + +#pragma mark - UILabel + +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; + [self setNeedsDisplay]; +} + +// Fixes crash when loading from a UIStoryboard +- (UIColor *)textColor { + UIColor *color = [super textColor]; + if (!color) { + color = [UIColor blackColor]; + } + + return color; +} + +- (void)setTextColor:(UIColor *)textColor { + UIColor *oldTextColor = self.textColor; + [super setTextColor:textColor]; + + // Redraw to allow any ColorFromContext attributes a chance to update + if (textColor != oldTextColor) { + [self setNeedsFramesetter]; + [self setNeedsDisplay]; + } +} + +- (CGRect)textRectForBounds:(CGRect)bounds + limitedToNumberOfLines:(NSInteger)numberOfLines +{ + bounds = UIEdgeInsetsInsetRect(bounds, self.textInsets); + if (!self.attributedText) { + return [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines]; + } + + CGRect textRect = bounds; + + // Calculate height with a minimum of double the font pointSize, to ensure that CTFramesetterSuggestFrameSizeWithConstraints doesn't return CGSizeZero, as it would if textRect height is insufficient. + textRect.size.height = MAX(self.font.lineHeight * MAX(2, numberOfLines), bounds.size.height); + + // Adjust the text to be in the center vertically, if the text size is smaller than bounds + CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints([self framesetter], CFRangeMake(0, (CFIndex)[self.attributedText length]), NULL, textRect.size, NULL); + textSize = CGSizeMake(CGFloat_ceil(textSize.width), CGFloat_ceil(textSize.height)); // Fix for iOS 4, CTFramesetterSuggestFrameSizeWithConstraints sometimes returns fractional sizes + + if (textSize.height < bounds.size.height) { + CGFloat yOffset = 0.0f; + switch (self.verticalAlignment) { + case BITAttributedLabelVerticalAlignmentCenter: + yOffset = CGFloat_floor((bounds.size.height - textSize.height) / 2.0f); + break; + case BITAttributedLabelVerticalAlignmentBottom: + yOffset = bounds.size.height - textSize.height; + break; + case BITAttributedLabelVerticalAlignmentTop: + default: + break; + } + + textRect.origin.y += yOffset; + } + + return textRect; +} + +- (void)drawTextInRect:(CGRect)rect { + CGRect insetRect = UIEdgeInsetsInsetRect(rect, self.textInsets); + if (!self.attributedText) { + [super drawTextInRect:insetRect]; + return; + } + + NSAttributedString *originalAttributedText = nil; + + // Adjust the font size to fit width, if necessarry + if (self.adjustsFontSizeToFitWidth && self.numberOfLines > 0) { + // Framesetter could still be working with a resized version of the text; + // need to reset so we start from the original font size. + // See #393. + [self setNeedsFramesetter]; + [self setNeedsDisplay]; + + if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) { + [self invalidateIntrinsicContentSize]; + } + + // Use infinite width to find the max width, which will be compared to availableWidth if needed. + CGSize maxSize = (self.numberOfLines > 1) ? CGSizeMake(BITFLOAT_MAX, BITFLOAT_MAX) : CGSizeZero; + + CGFloat textWidth = [self sizeThatFits:maxSize].width; + CGFloat availableWidth = self.frame.size.width * self.numberOfLines; + if (self.numberOfLines > 1 && self.lineBreakMode == BITLineBreakByWordWrapping) { + textWidth *= kBITLineBreakWordWrapTextWidthScalingFactor; + } + + if (textWidth > availableWidth && textWidth > 0.0f) { + originalAttributedText = [self.attributedText copy]; + + CGFloat scaleFactor = availableWidth / textWidth; + if ([self respondsToSelector:@selector(minimumScaleFactor)] && self.minimumScaleFactor > scaleFactor) { + scaleFactor = self.minimumScaleFactor; + } + + self.attributedText = NSAttributedStringByScalingFontSize(self.attributedText, scaleFactor); + } + } + + CGContextRef c = UIGraphicsGetCurrentContext(); + CGContextSaveGState(c); + { + CGContextSetTextMatrix(c, CGAffineTransformIdentity); + + // Inverts the CTM to match iOS coordinates (otherwise text draws upside-down; Mac OS's system is different) + CGContextTranslateCTM(c, 0.0f, insetRect.size.height); + CGContextScaleCTM(c, 1.0f, -1.0f); + + CFRange textRange = CFRangeMake(0, (CFIndex)[self.attributedText length]); + + // First, get the text rect (which takes vertical centering into account) + CGRect textRect = [self textRectForBounds:rect limitedToNumberOfLines:self.numberOfLines]; + + // CoreText draws its text aligned to the bottom, so we move the CTM here to take our vertical offsets into account + CGContextTranslateCTM(c, insetRect.origin.x, insetRect.size.height - textRect.origin.y - textRect.size.height); + + // Second, trace the shadow before the actual text, if we have one + if (self.shadowColor && !self.highlighted) { + CGContextSetShadowWithColor(c, self.shadowOffset, self.shadowRadius, [self.shadowColor CGColor]); + } else if (self.highlightedShadowColor) { + CGContextSetShadowWithColor(c, self.highlightedShadowOffset, self.highlightedShadowRadius, [self.highlightedShadowColor CGColor]); + } + + // Finally, draw the text or highlighted text itself (on top of the shadow, if there is one) + if (self.highlightedTextColor && self.highlighted) { + NSMutableAttributedString *highlightAttributedString = [self.renderedAttributedText mutableCopy]; + [highlightAttributedString addAttribute:(__bridge NSString *)kCTForegroundColorAttributeName value:(id)[self.highlightedTextColor CGColor] range:NSMakeRange(0, highlightAttributedString.length)]; + + if (![self highlightFramesetter]) { + CTFramesetterRef highlightFramesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)highlightAttributedString); + [self setHighlightFramesetter:highlightFramesetter]; + CFRelease(highlightFramesetter); + } + + [self drawFramesetter:[self highlightFramesetter] attributedString:highlightAttributedString textRange:textRange inRect:textRect context:c]; + } else { + [self drawFramesetter:[self framesetter] attributedString:self.renderedAttributedText textRange:textRange inRect:textRect context:c]; + } + + // If we adjusted the font size, set it back to its original size + if (originalAttributedText) { + // Use ivar directly to avoid clearing out framesetter and renderedAttributedText + _attributedText = originalAttributedText; + } + } + CGContextRestoreGState(c); +} + +#pragma mark - UIAccessibilityElement + +- (BOOL)isAccessibilityElement { + return NO; +} + +- (NSInteger)accessibilityElementCount { + return (NSInteger)[[self accessibilityElements] count]; +} + +- (id)accessibilityElementAtIndex:(NSInteger)index { + return [[self accessibilityElements] objectAtIndex:(NSUInteger)index]; +} + +- (NSInteger)indexOfAccessibilityElement:(id)element { + return (NSInteger)[[self accessibilityElements] indexOfObject:element]; +} + +- (NSArray *)accessibilityElements { + if (!_accessibilityElements) { + @synchronized(self) { + NSMutableArray *mutableAccessibilityItems = [NSMutableArray array]; + + for (BITAttributedLabelLink *link in self.linkModels) { + + if (link.result.range.location == NSNotFound) { + continue; + } + + NSString *sourceText = [self.text isKindOfClass:[NSString class]] ? self.text : [(NSAttributedString *)self.text string]; + + NSString *accessibilityLabel = [sourceText substringWithRange:link.result.range]; + NSString *accessibilityValue = link.accessibilityValue; + + if (accessibilityLabel) { + BITAccessibilityElement *linkElement = [[BITAccessibilityElement alloc] initWithAccessibilityContainer:self]; + linkElement.accessibilityTraits = UIAccessibilityTraitLink; + linkElement.boundingRect = [self boundingRectForCharacterRange:link.result.range]; + linkElement.superview = self; + linkElement.accessibilityLabel = accessibilityLabel; + + if (![accessibilityLabel isEqualToString:accessibilityValue]) { + linkElement.accessibilityValue = accessibilityValue; + } + + [mutableAccessibilityItems addObject:linkElement]; + } + } + + BITAccessibilityElement *baseElement = [[BITAccessibilityElement alloc] initWithAccessibilityContainer:self]; + baseElement.accessibilityLabel = [super accessibilityLabel]; + baseElement.accessibilityHint = [super accessibilityHint]; + baseElement.accessibilityValue = [super accessibilityValue]; + baseElement.boundingRect = self.bounds; + baseElement.superview = self; + baseElement.accessibilityTraits = [super accessibilityTraits]; + + [mutableAccessibilityItems addObject:baseElement]; + + self.accessibilityElements = [NSArray arrayWithArray:mutableAccessibilityItems]; + } + } + + return _accessibilityElements; +} + +#pragma mark - UIView + +- (CGSize)sizeThatFits:(CGSize)size { + if (!self.attributedText) { + return [super sizeThatFits:size]; + } else { + NSAttributedString *string = [self renderedAttributedText]; + + CGSize labelSize = CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints([self framesetter], string, size, (NSUInteger)self.numberOfLines); + labelSize.width += self.textInsets.left + self.textInsets.right; + labelSize.height += self.textInsets.top + self.textInsets.bottom; + + return labelSize; + } +} + +- (CGSize)intrinsicContentSize { + // There's an implicit width from the original UILabel implementation + return [self sizeThatFits:[super intrinsicContentSize]]; +} + +- (void)tintColorDidChange { + if (!self.inactiveLinkAttributes || [self.inactiveLinkAttributes count] == 0) { + return; + } + + BOOL isInactive = (self.tintAdjustmentMode == UIViewTintAdjustmentModeDimmed); + + NSMutableAttributedString *mutableAttributedString = [self.attributedText mutableCopy]; + for (BITAttributedLabelLink *link in self.linkModels) { + NSDictionary *attributesToRemove = isInactive ? link.attributes : link.inactiveAttributes; + NSDictionary *attributesToAdd = isInactive ? link.inactiveAttributes : link.attributes; + + [attributesToRemove enumerateKeysAndObjectsUsingBlock:^(NSString *name, __unused id value, __unused BOOL *stop) { + if (NSMaxRange(link.result.range) <= mutableAttributedString.length) { + [mutableAttributedString removeAttribute:name range:link.result.range]; + } + }]; + + if (attributesToAdd) { + if (NSMaxRange(link.result.range) <= mutableAttributedString.length) { + [mutableAttributedString addAttributes:attributesToAdd range:link.result.range]; + } + } + } + + self.attributedText = mutableAttributedString; + + [self setNeedsDisplay]; +} + +- (UIView *)hitTest:(CGPoint)point + withEvent:(UIEvent *)event +{ + if (![self linkAtPoint:point] || !self.userInteractionEnabled || self.hidden || self.alpha < 0.01) { + return [super hitTest:point withEvent:event]; + } + + return self; +} + +#pragma mark - UIResponder + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +- (BOOL)canPerformAction:(SEL)action + withSender:(__unused id)sender +{ +#if !TARGET_OS_TV + return (action == @selector(copy:)); +#else + return NO; +#endif +} + +- (void)touchesBegan:(NSSet *)touches + withEvent:(UIEvent *)event +{ + UITouch *touch = [touches anyObject]; + + self.activeLink = [self linkAtPoint:[touch locationInView:self]]; + + if (!self.activeLink) { + [super touchesBegan:touches withEvent:event]; + } +} + +- (void)touchesMoved:(NSSet *)touches + withEvent:(UIEvent *)event +{ + if (self.activeLink) { + UITouch *touch = [touches anyObject]; + + if (self.activeLink != [self linkAtPoint:[touch locationInView:self]]) { + self.activeLink = nil; + } + } else { + [super touchesMoved:touches withEvent:event]; + } +} + +- (void)touchesEnded:(NSSet *)touches + withEvent:(UIEvent *)event +{ + if (self.activeLink) { + if (self.activeLink.linkTapBlock) { + self.activeLink.linkTapBlock(self, self.activeLink); + self.activeLink = nil; + return; + } + + NSTextCheckingResult *result = self.activeLink.result; + self.activeLink = nil; + + switch (result.resultType) { + case NSTextCheckingTypeLink: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithURL:)]) { + [self.delegate attributedLabel:self didSelectLinkWithURL:result.URL]; + return; + } + break; + case NSTextCheckingTypeAddress: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithAddress:)]) { + [self.delegate attributedLabel:self didSelectLinkWithAddress:result.addressComponents]; + return; + } + break; + case NSTextCheckingTypePhoneNumber: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithPhoneNumber:)]) { + [self.delegate attributedLabel:self didSelectLinkWithPhoneNumber:result.phoneNumber]; + return; + } + break; + case NSTextCheckingTypeDate: + if (result.timeZone && [self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithDate:timeZone:duration:)]) { + [self.delegate attributedLabel:self didSelectLinkWithDate:result.date timeZone:result.timeZone duration:result.duration]; + return; + } else if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithDate:)]) { + [self.delegate attributedLabel:self didSelectLinkWithDate:result.date]; + return; + } + break; + case NSTextCheckingTypeTransitInformation: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithTransitInformation:)]) { + [self.delegate attributedLabel:self didSelectLinkWithTransitInformation:result.components]; + return; + } + default: + break; + } + + // Fallback to `attributedLabel:didSelectLinkWithTextCheckingResult:` if no other delegate method matched. + if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectLinkWithTextCheckingResult:)]) { + [self.delegate attributedLabel:self didSelectLinkWithTextCheckingResult:result]; + } + } else { + [super touchesEnded:touches withEvent:event]; + } +} + +- (void)touchesCancelled:(NSSet *)touches + withEvent:(UIEvent *)event +{ + if (self.activeLink) { + self.activeLink = nil; + } else { + [super touchesCancelled:touches withEvent:event]; + } +} + +#pragma mark - UIGestureRecognizerDelegate + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { + return [self containslinkAtPoint:[touch locationInView:self]]; +} + +#pragma mark - UILongPressGestureRecognizer + +- (void)longPressGestureDidFire:(UILongPressGestureRecognizer *)sender { + switch (sender.state) { + case UIGestureRecognizerStateBegan: { + CGPoint touchPoint = [sender locationInView:self]; + BITAttributedLabelLink *link = [self linkAtPoint:touchPoint]; + + if (link) { + if (link.linkLongPressBlock) { + link.linkLongPressBlock(self, link); + return; + } + + NSTextCheckingResult *result = link.result; + + if (!result) { + return; + } + + switch (result.resultType) { + case NSTextCheckingTypeLink: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithURL:atPoint:)]) { + [self.delegate attributedLabel:self didLongPressLinkWithURL:result.URL atPoint:touchPoint]; + return; + } + break; + case NSTextCheckingTypeAddress: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithAddress:atPoint:)]) { + [self.delegate attributedLabel:self didLongPressLinkWithAddress:result.addressComponents atPoint:touchPoint]; + return; + } + break; + case NSTextCheckingTypePhoneNumber: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithPhoneNumber:atPoint:)]) { + [self.delegate attributedLabel:self didLongPressLinkWithPhoneNumber:result.phoneNumber atPoint:touchPoint]; + return; + } + break; + case NSTextCheckingTypeDate: + if (result.timeZone && [self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithDate:timeZone:duration:atPoint:)]) { + [self.delegate attributedLabel:self didLongPressLinkWithDate:result.date timeZone:result.timeZone duration:result.duration atPoint:touchPoint]; + return; + } else if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithDate:atPoint:)]) { + [self.delegate attributedLabel:self didLongPressLinkWithDate:result.date atPoint:touchPoint]; + return; + } + break; + case NSTextCheckingTypeTransitInformation: + if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithTransitInformation:atPoint:)]) { + [self.delegate attributedLabel:self didLongPressLinkWithTransitInformation:result.components atPoint:touchPoint]; + return; + } + default: + break; + } + + // Fallback to `attributedLabel:didLongPressLinkWithTextCheckingResult:atPoint:` if no other delegate method matched. + if ([self.delegate respondsToSelector:@selector(attributedLabel:didLongPressLinkWithTextCheckingResult:atPoint:)]) { + [self.delegate attributedLabel:self didLongPressLinkWithTextCheckingResult:result atPoint:touchPoint]; + } + } + break; + } + default: + break; + } +} + +#if !TARGET_OS_TV +#pragma mark - UIResponderStandardEditActions + +- (void)copy:(__unused id)sender { + [[UIPasteboard generalPasteboard] setString:self.text]; +} +#endif + +#pragma mark - NSCoding + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + + [coder encodeObject:@(self.enabledTextCheckingTypes) forKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))]; + + [coder encodeObject:self.linkModels forKey:NSStringFromSelector(@selector(linkModels))]; + if ([NSMutableParagraphStyle class]) { + [coder encodeObject:self.linkAttributes forKey:NSStringFromSelector(@selector(linkAttributes))]; + [coder encodeObject:self.activeLinkAttributes forKey:NSStringFromSelector(@selector(activeLinkAttributes))]; + [coder encodeObject:self.inactiveLinkAttributes forKey:NSStringFromSelector(@selector(inactiveLinkAttributes))]; + } + [coder encodeObject:@(self.shadowRadius) forKey:NSStringFromSelector(@selector(shadowRadius))]; + [coder encodeObject:@(self.highlightedShadowRadius) forKey:NSStringFromSelector(@selector(highlightedShadowRadius))]; + [coder encodeCGSize:self.highlightedShadowOffset forKey:NSStringFromSelector(@selector(highlightedShadowOffset))]; + [coder encodeObject:self.highlightedShadowColor forKey:NSStringFromSelector(@selector(highlightedShadowColor))]; + [coder encodeObject:@(self.kern) forKey:NSStringFromSelector(@selector(kern))]; + [coder encodeObject:@(self.firstLineIndent) forKey:NSStringFromSelector(@selector(firstLineIndent))]; + [coder encodeObject:@(self.lineSpacing) forKey:NSStringFromSelector(@selector(lineSpacing))]; + [coder encodeObject:@(self.lineHeightMultiple) forKey:NSStringFromSelector(@selector(lineHeightMultiple))]; + [coder encodeUIEdgeInsets:self.textInsets forKey:NSStringFromSelector(@selector(textInsets))]; + [coder encodeInteger:self.verticalAlignment forKey:NSStringFromSelector(@selector(verticalAlignment))]; + + [coder encodeObject:self.attributedTruncationToken forKey:NSStringFromSelector(@selector(attributedTruncationToken))]; + + [coder encodeObject:NSStringFromUIEdgeInsets(self.linkBackgroundEdgeInset) forKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]; + [coder encodeObject:self.attributedText forKey:NSStringFromSelector(@selector(attributedText))]; + [coder encodeObject:self.text forKey:NSStringFromSelector(@selector(text))]; +} + +- (id)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (!self) { + return nil; + } + + [self commonInit]; + + if ([coder containsValueForKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))]) { + self.enabledTextCheckingTypes = [[coder decodeObjectForKey:NSStringFromSelector(@selector(enabledTextCheckingTypes))] unsignedLongLongValue]; + } + + if ([NSMutableParagraphStyle class]) { + if ([coder containsValueForKey:NSStringFromSelector(@selector(linkAttributes))]) { + self.linkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkAttributes))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(activeLinkAttributes))]) { + self.activeLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(activeLinkAttributes))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))]) { + self.inactiveLinkAttributes = [coder decodeObjectForKey:NSStringFromSelector(@selector(inactiveLinkAttributes))]; + } + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(links))]) { + NSArray *oldLinks = [coder decodeObjectForKey:NSStringFromSelector(@selector(links))]; + [self addLinksWithTextCheckingResults:oldLinks attributes:nil]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(linkModels))]) { + self.linkModels = [coder decodeObjectForKey:NSStringFromSelector(@selector(linkModels))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(shadowRadius))]) { + self.shadowRadius = [[coder decodeObjectForKey:NSStringFromSelector(@selector(shadowRadius))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowRadius))]) { + self.highlightedShadowRadius = [[coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowRadius))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowOffset))]) { + self.highlightedShadowOffset = [coder decodeCGSizeForKey:NSStringFromSelector(@selector(highlightedShadowOffset))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(highlightedShadowColor))]) { + self.highlightedShadowColor = [coder decodeObjectForKey:NSStringFromSelector(@selector(highlightedShadowColor))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(kern))]) { + self.kern = [[coder decodeObjectForKey:NSStringFromSelector(@selector(kern))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(firstLineIndent))]) { + self.firstLineIndent = [[coder decodeObjectForKey:NSStringFromSelector(@selector(firstLineIndent))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(lineSpacing))]) { + self.lineSpacing = [[coder decodeObjectForKey:NSStringFromSelector(@selector(lineSpacing))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(minimumLineHeight))]) { + self.minimumLineHeight = [[coder decodeObjectForKey:NSStringFromSelector(@selector(minimumLineHeight))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(maximumLineHeight))]) { + self.maximumLineHeight = [[coder decodeObjectForKey:NSStringFromSelector(@selector(maximumLineHeight))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(lineHeightMultiple))]) { + self.lineHeightMultiple = [[coder decodeObjectForKey:NSStringFromSelector(@selector(lineHeightMultiple))] floatValue]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(textInsets))]) { + self.textInsets = [coder decodeUIEdgeInsetsForKey:NSStringFromSelector(@selector(textInsets))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(verticalAlignment))]) { + self.verticalAlignment = [coder decodeIntegerForKey:NSStringFromSelector(@selector(verticalAlignment))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(attributedTruncationToken))]) { + self.attributedTruncationToken = [coder decodeObjectForKey:NSStringFromSelector(@selector(attributedTruncationToken))]; + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]) { + self.linkBackgroundEdgeInset = UIEdgeInsetsFromString([coder decodeObjectForKey:NSStringFromSelector(@selector(linkBackgroundEdgeInset))]); + } + + if ([coder containsValueForKey:NSStringFromSelector(@selector(attributedText))]) { + self.attributedText = [coder decodeObjectForKey:NSStringFromSelector(@selector(attributedText))]; + } else { + self.text = super.text; + } + + return self; +} + +@end + +#pragma mark - BITAttributedLabelLink + +@implementation BITAttributedLabelLink + +- (instancetype)initWithAttributes:(NSDictionary *)attributes + activeAttributes:(NSDictionary *)activeAttributes + inactiveAttributes:(NSDictionary *)inactiveAttributes + textCheckingResult:(NSTextCheckingResult *)result { + + if ((self = [super init])) { + _result = result; + _attributes = [attributes copy]; + _activeAttributes = [activeAttributes copy]; + _inactiveAttributes = [inactiveAttributes copy]; + } + + return self; +} + +- (instancetype)initWithAttributesFromLabel:(BITAttributedLabel*)label + textCheckingResult:(NSTextCheckingResult *)result { + + return [self initWithAttributes:label.linkAttributes + activeAttributes:label.activeLinkAttributes + inactiveAttributes:label.inactiveLinkAttributes + textCheckingResult:result]; +} + +#pragma mark - Accessibility + +- (NSString *) accessibilityValue { + if ([_accessibilityValue length] == 0) { + switch (self.result.resultType) { + case NSTextCheckingTypeLink: + _accessibilityValue = self.result.URL.absoluteString; + break; + case NSTextCheckingTypePhoneNumber: + _accessibilityValue = self.result.phoneNumber; + break; + case NSTextCheckingTypeDate: + _accessibilityValue = [NSDateFormatter localizedStringFromDate:self.result.date + dateStyle:NSDateFormatterLongStyle + timeStyle:NSDateFormatterLongStyle]; + break; + default: + break; + } + } + + return _accessibilityValue; +} + +#pragma mark - NSCoding + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.result forKey:NSStringFromSelector(@selector(result))]; + [aCoder encodeObject:self.attributes forKey:NSStringFromSelector(@selector(attributes))]; + [aCoder encodeObject:self.activeAttributes forKey:NSStringFromSelector(@selector(activeAttributes))]; + [aCoder encodeObject:self.inactiveAttributes forKey:NSStringFromSelector(@selector(inactiveAttributes))]; + [aCoder encodeObject:self.accessibilityValue forKey:NSStringFromSelector(@selector(accessibilityValue))]; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + if ((self = [super init])) { + _result = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(result))]; + _attributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(attributes))]; + _activeAttributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(activeAttributes))]; + _inactiveAttributes = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(inactiveAttributes))]; + self.accessibilityValue = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(accessibilityValue))]; + } + + return self; +} + +@end + +#pragma mark - + +static inline CGColorRef CGColorRefFromColor(id color) { + return [color isKindOfClass:[UIColor class]] ? [color CGColor] : (__bridge CGColorRef)color; +} + +static inline CTFontRef CTFontRefFromUIFont(UIFont * font) { + CTFontRef ctfont = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL); + return CFAutorelease(ctfont); +} + +static inline NSDictionary * convertNSAttributedStringAttributesToCTAttributes(NSDictionary *attributes) { + if (!attributes) return nil; + + NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionary]; + + NSDictionary *NSToCTAttributeNamesMap = @{ + NSFontAttributeName: (NSString *)kCTFontAttributeName, + NSBackgroundColorAttributeName: (NSString *)kBITBackgroundFillColorAttributeName, + NSForegroundColorAttributeName: (NSString *)kCTForegroundColorAttributeName, + NSUnderlineColorAttributeName: (NSString *)kCTUnderlineColorAttributeName, + NSUnderlineStyleAttributeName: (NSString *)kCTUnderlineStyleAttributeName, + NSStrokeWidthAttributeName: (NSString *)kCTStrokeWidthAttributeName, + NSStrokeColorAttributeName: (NSString *)kCTStrokeWidthAttributeName, + NSKernAttributeName: (NSString *)kCTKernAttributeName, + NSLigatureAttributeName: (NSString *)kCTLigatureAttributeName + }; + + [attributes enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + key = [NSToCTAttributeNamesMap objectForKey:key] ? : key; + + if (![NSMutableParagraphStyle class]) { + if ([value isKindOfClass:[UIFont class]]) { + value = (__bridge id)CTFontRefFromUIFont(value); + } else if ([value isKindOfClass:[UIColor class]]) { + value = (__bridge id)((UIColor *)value).CGColor; + } + } + + [mutableAttributes setObject:value forKey:key]; + }]; + + return [NSDictionary dictionaryWithDictionary:mutableAttributes]; +} + +#pragma clang diagnostic pop + +#endif diff --git a/submodules/HockeySDK-iOS/Classes/BITAuthenticationViewController.h b/submodules/HockeySDK-iOS/Classes/BITAuthenticationViewController.h new file mode 100644 index 0000000000..2eac237f18 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITAuthenticationViewController.h @@ -0,0 +1,94 @@ +/* + * Author: Stephan Diederich + * + * Copyright (c) 2013-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 +@protocol BITAuthenticationViewControllerDelegate; +@class BITAuthenticator; +@class BITHockeyAppClient; + +/** + * View controller handling user interaction for `BITAuthenticator` + */ +@interface BITAuthenticationViewController : UITableViewController + +- (instancetype) initWithDelegate:(id) delegate; + +/** + * can be set to YES to show an additional button + description text + * and allowing to login via external website/UDID. + * if this is set to yes, no further email/password options are shown + * + * defaults to NO + */ +@property (nonatomic, assign) BOOL showsLoginViaWebButton; + +/** + * Description shown on top of view. Should tell why this view + * was presented and what's next. + */ +@property (nonatomic, copy) NSString* tableViewTitle; + +/** + * can be set to YES to also require the users password + * + * defaults to NO + */ +@property (nonatomic, assign) BOOL requirePassword; + +@property (nonatomic, weak) id delegate; + +/** + * allows to pre-fill the email-addy + */ +@property (nonatomic, copy) NSString* email; +@end + +/** + * BITAuthenticationViewController protocol + */ +@protocol BITAuthenticationViewControllerDelegate + +- (void) authenticationViewControllerDidTapWebButton:(UIViewController*) viewController; + +/** + * called when the user wants to login + * + * @param viewController the delegating view controller + * @param email the content of the email-field + * @param password the content of the password-field (if existent) + * @param completion Must be called by the delegate once the auth-task completed + * This view controller shows an activity-indicator in between and blocks + * the UI. if succeeded is NO, it shows an alertView presenting the error + * given by the completion block + */ +- (void) authenticationViewController:(UIViewController*) viewController + handleAuthenticationWithEmail:(NSString*) email + password:(NSString*) password + completion:(void(^)(BOOL succeeded, NSError *error)) completion; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITAuthenticationViewController.m b/submodules/HockeySDK-iOS/Classes/BITAuthenticationViewController.m new file mode 100644 index 0000000000..bcf765683a --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITAuthenticationViewController.m @@ -0,0 +1,321 @@ +/* + * Author: Stephan Diederich + * + * Copyright (c) 2013-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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + +#import "BITAuthenticationViewController.h" +#import "BITAuthenticator_Private.h" +#import "HockeySDKPrivate.h" +#import "BITHockeyHelper.h" +#import "BITHockeyAppClient.h" +#import + +@interface BITAuthenticationViewController () + +@property (nonatomic, weak) UITextField *emailField; +@property (nonatomic, copy) NSString *password; + +@end + +@implementation BITAuthenticationViewController + +- (instancetype) initWithDelegate:(id)delegate { + self = [super initWithStyle:UITableViewStyleGrouped]; + if (self) { + self.title = BITHockeyLocalizedString(@"HockeyAuthenticatorViewControllerTitle"); + _delegate = delegate; + } + return self; +} + +#pragma mark - view lifecycle + +- (void)viewDidLoad { + [super viewDidLoad]; + + [self.tableView setScrollEnabled:NO]; + + [self updateWebLoginButton]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + [self updateBarButtons]; + + self.navigationItem.rightBarButtonItem.enabled = [self allRequiredFieldsEntered]; +} + +#pragma mark - Property overrides + +- (void) updateBarButtons { + if(self.showsLoginViaWebButton) { + self.navigationItem.rightBarButtonItem = nil; + } else { + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone + target:self + action:@selector(saveAction:)]; + } +} + +- (void)setShowsLoginViaWebButton:(BOOL)showsLoginViaWebButton { + if(_showsLoginViaWebButton != showsLoginViaWebButton) { + _showsLoginViaWebButton = showsLoginViaWebButton; + if(self.isViewLoaded) { + [self.tableView reloadData]; + [self updateBarButtons]; + [self updateWebLoginButton]; + } + } +} + +- (void) updateWebLoginButton { + if(self.showsLoginViaWebButton) { + static const CGFloat kFooterHeight = 60.0; + UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, + CGRectGetWidth(self.tableView.bounds), + kFooterHeight)]; + UIButton *button = [UIButton buttonWithType:kBITButtonTypeSystem]; + [button setTitle:BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerWebLoginButtonTitle") forState:UIControlStateNormal]; + CGSize buttonSize = [button sizeThatFits:CGSizeMake(CGRectGetWidth(self.tableView.bounds), + kFooterHeight)]; + button.frame = CGRectMake(floor((CGRectGetWidth(containerView.bounds) - buttonSize.width) / (CGFloat)2.0), + floor((kFooterHeight - buttonSize.height) / (CGFloat)2.0), + buttonSize.width, + buttonSize.height); + button.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + if ([UIButton instancesRespondToSelector:(NSSelectorFromString(@"setTintColor:"))]) { + [button setTitleColor:BIT_RGBCOLOR(0, 122, 255) forState:UIControlStateNormal]; + } + [containerView addSubview:button]; + [button addTarget:self + action:@selector(handleWebLoginButton:) + forControlEvents:UIControlEventTouchUpInside]; + self.tableView.tableFooterView = containerView; + } else { + self.tableView.tableFooterView = nil; + } +} + +- (IBAction) handleWebLoginButton:(id) __unused sender { + [self.delegate authenticationViewControllerDidTapWebButton:self]; +} + +- (void)setEmail:(NSString *)email { + _email = email; + if(self.isViewLoaded) { + self.emailField.text = email; + } +} + +- (void)setTableViewTitle:(NSString *)viewDescription { + _tableViewTitle = [viewDescription copy]; + if(self.isViewLoaded) { + [self.tableView reloadData]; + } +} +#pragma mark - UIViewController Rotation + +-(UIInterfaceOrientationMask)supportedInterfaceOrientations { + return UIInterfaceOrientationMaskAll; +} + +#pragma mark - Private methods +- (BOOL)allRequiredFieldsEntered { + if (self.requirePassword && [self.password length] == 0) + return NO; + + if (![self.email length] || !bit_validateEmail(self.email)) + return NO; + + return YES; +} + +#pragma mark - Table view data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView *) __unused tableView { + return 2; +} + +- (NSInteger)tableView:(UITableView *) __unused tableView numberOfRowsInSection:(NSInteger)section { + if (section == 0) return 0; + + if(self.showsLoginViaWebButton) { + return 0; + } else { + NSInteger rows = 1; + + if ([self requirePassword]) rows ++; + + return rows; + } +} + +- (NSString *)tableView:(UITableView *) __unused tableView titleForFooterInSection:(NSInteger)section { + if (section == 0) { + return self.tableViewTitle; + } + + return nil; +} + +- (UITableViewCell *)tableView:(UITableView *) __unused tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *CellIdentifier = @"InputCell"; + + UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier]; + if (cell == nil) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; + + cell.accessoryType = UITableViewCellAccessoryNone; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + cell.backgroundColor = [UIColor whiteColor]; + + UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(130, 11, self.view.frame.size.width - 130 - 25, 24)]; + if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) { + textField.autoresizingMask = UIViewAutoresizingFlexibleWidth; + } + textField.adjustsFontSizeToFitWidth = YES; + textField.textColor = [UIColor blackColor]; + textField.backgroundColor = [UIColor lightGrayColor]; + + if (0 == [indexPath row]) { + textField.placeholder = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerEmailPlaceholder"); + textField.accessibilityHint = BITHockeyLocalizedString(@"HockeyAccessibilityHintRequired"); + textField.text = self.email; + self.emailField = textField; + + textField.keyboardType = UIKeyboardTypeEmailAddress; + if ([self requirePassword]) + textField.returnKeyType = UIReturnKeyNext; + else + textField.returnKeyType = UIReturnKeyDone; + + [textField addTarget:self action:@selector(userEmailEntered:) forControlEvents:UIControlEventEditingChanged]; + [textField becomeFirstResponder]; + } else { + textField.placeholder = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerPasswordPlaceholder"); + textField.text = self.password; + + textField.keyboardType = UIKeyboardTypeDefault; + textField.returnKeyType = UIReturnKeyDone; + textField.secureTextEntry = YES; + [textField addTarget:self action:@selector(userPasswordEntered:) forControlEvents:UIControlEventEditingChanged]; + } + + textField.backgroundColor = [UIColor whiteColor]; + textField.autocorrectionType = UITextAutocorrectionTypeNo; + textField.autocapitalizationType = UITextAutocapitalizationTypeNone; + textField.textAlignment = NSTextAlignmentLeft; + textField.delegate = self; + textField.tag = indexPath.row; + + textField.clearButtonMode = UITextFieldViewModeWhileEditing; + [textField setEnabled: YES]; + + [cell addSubview:textField]; + } + + if (0 == [indexPath row]) { + cell.textLabel.text = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerEmailDescription"); + } else { + cell.textLabel.text = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerPasswordDescription"); + } + + return cell; +} + + +- (void)userEmailEntered:(id)sender { + self.email = [(UITextField *)sender text]; + + self.navigationItem.rightBarButtonItem.enabled = [self allRequiredFieldsEntered]; +} + +- (void)userPasswordEntered:(id)sender { + self.password = [(UITextField *)sender text]; + + self.navigationItem.rightBarButtonItem.enabled = [self allRequiredFieldsEntered]; +} + +#pragma mark - UITextFieldDelegate + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + NSInteger nextTag = textField.tag + 1; + + UIResponder* nextResponder = [self.view viewWithTag:nextTag]; + if (nextResponder) { + [nextResponder becomeFirstResponder]; + } else { + if ([self allRequiredFieldsEntered]) { + if ([textField isFirstResponder]) + [textField resignFirstResponder]; + + [self saveAction:nil]; + } + } + return NO; +} + +#pragma mark - Actions +- (void)saveAction:(id) __unused sender { + [self setLoginUIEnabled:NO]; + + __weak typeof(self) weakSelf = self; + [self.delegate authenticationViewController:self + handleAuthenticationWithEmail:self.email + password:self.password + completion:^(BOOL succeeded, NSError *error) { + if(succeeded) { + //controller should dismiss us shortly.. + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil + message:error.localizedDescription + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *okAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"OK") + style:UIAlertActionStyleCancel + handler:^(UIAlertAction __unused *action) {}]; + [alertController addAction:okAction]; + [self presentViewController:alertController animated:YES completion:nil]; + typeof(self) strongSelf = weakSelf; + [strongSelf setLoginUIEnabled:YES]; + }); + } + }]; +} + +- (void) setLoginUIEnabled:(BOOL) enabled { + self.navigationItem.rightBarButtonItem.enabled = enabled; + self.tableView.userInteractionEnabled = enabled; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ diff --git a/submodules/HockeySDK-iOS/Classes/BITAuthenticator.h b/submodules/HockeySDK-iOS/Classes/BITAuthenticator.h new file mode 100644 index 0000000000..a1eaac1ad6 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITAuthenticator.h @@ -0,0 +1,385 @@ +/* + * Author: Stephan Diederich, Andreas Linde + * + * Copyright (c) 2013-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 + +#import "BITHockeyBaseManager.h" + +/** + * Identification Types + */ +typedef NS_ENUM(NSUInteger, BITAuthenticatorIdentificationType) { + /** + * Assigns this app an anonymous user id. + *

+ * The user will not be asked anything and an anonymous ID will be generated. + * This helps identifying this installation being unique but HockeyApp won't be able + * to identify who actually is running this installation and on which device + * the app is installed. + */ + BITAuthenticatorIdentificationTypeAnonymous, + /** + * Ask for the HockeyApp account email + *

+ * This will present a user interface requesting the user to provide their + * HockeyApp user email address. + *

+ * The provided email address has to match an email address of a registered + * HockeyApp user who is a member or tester of the app + */ + BITAuthenticatorIdentificationTypeHockeyAppEmail, + /** + * Ask for the HockeyApp account by email and password + *

+ * This will present a user interface requesting the user to provide their + * HockeyApp user credentials. + *

+ * The provided user account has to match a registered HockeyApp user who is + * a member or tester of the app + */ + BITAuthenticatorIdentificationTypeHockeyAppUser, + /** + * Identifies the current device + *

+ * This will open the HockeyApp web page on the device in Safari and request the user + * to submit the device's unique identifier to the app. If the web page session is not aware + * of the current devices UDID, it will request the user to install the HockeyApp web clip + * which will provide the UDID to users session in the browser. + *

+ * This requires the app to register an URL scheme. See the linked property and methods + * for further documentation on this. + */ + BITAuthenticatorIdentificationTypeDevice, + /** + * Ask for the HockeyApp account email. + *

+ * This will present a user interface requesting the user to start a Safari based + * flow to login to HockeyApp (if not already logged in) and to share the hockeyapp + * account's email. + *

+ * If restrictApplicationUsage is enabled, the provided user account has to match a + * registered HockeyApp user who is a member or tester of the app. + * For identification purpose any HockeyApp user is allowed. + */ + BITAuthenticatorIdentificationTypeWebAuth, +}; + +/** + * Restriction enforcement styles + * + * Specifies how often the Authenticator checks if the user is allowed to use + * this app. + */ +typedef NS_ENUM(NSUInteger, BITAuthenticatorAppRestrictionEnforcementFrequency) { + /** + * Checks if the user is allowed to use the app at the first time a version is started + */ + BITAuthenticatorAppRestrictionEnforcementOnFirstLaunch, + /** + * Checks if the user is allowed to use the app every time the app becomes active + */ + BITAuthenticatorAppRestrictionEnforcementOnAppActive, +}; + +@protocol BITAuthenticatorDelegate; + + +/** + * Identify and authenticate users of Ad-Hoc or Enterprise builds + * + * `BITAuthenticator` serves 2 purposes: + * + * 1. Identifying who is running your Ad-Hoc or Enterprise builds + * `BITAuthenticator` provides an identifier for the rest of the HockeySDK + * to work with, e.g. in-app update checks and crash reports. + * + * 2. Optional regular checking if an identified user is still allowed + * to run this application. The `BITAuthenticator` can be used to make + * sure only users who are testers of your app are allowed to run it. + * + * This module automatically disables itself when running in an App Store build by default! + * + * @warning It is mandatory to call `authenticateInstallation` somewhen after calling + * `[[BITHockeyManager sharedHockeyManager] startManager]` or fully customize the identification + * and validation workflow yourself. + * If your app shows a modal view on startup, make sure to call `authenticateInstallation` + * either once your modal view is fully presented (e.g. its `viewDidLoad:` method is processed) + * or once your modal view is dismissed. + */ +@interface BITAuthenticator : BITHockeyBaseManager + +#pragma mark - Configuration + + +///----------------------------------------------------------------------------- +/// @name Configuration +///----------------------------------------------------------------------------- + + +/** + * Defines the identification mechanism to be used + * + * _Default_: `BITAuthenticatorIdentificationTypeAnonymous` + * + * @see BITAuthenticatorIdentificationType + */ +@property (nonatomic, assign) BITAuthenticatorIdentificationType identificationType; + + +/** + * Enables or disables checking if the user is allowed to run this app + * + * If disabled, the Authenticator never validates, besides initial identification, + * if the user is allowed to run this application. + * + * If enabled, the Authenticator checks depending on `restrictionEnforcementFrequency` + * if the user is allowed to use this application. + * + * Enabling this property and setting `identificationType` to `BITAuthenticatorIdentificationTypeHockeyAppEmail`, + * `BITAuthenticatorIdentificationTypeHockeyAppUser` or `BITAuthenticatorIdentificationTypeWebAuth` also allows + * to remove access for users by removing them from the app's users list on HockeyApp. + * + * _Default_: `NO` + * + * @warning if `identificationType` is set to `BITAuthenticatorIdentificationTypeAnonymous`, + * this property has no effect. + * + * @see BITAuthenticatorIdentificationType + * @see restrictionEnforcementFrequency + */ +@property (nonatomic, assign) BOOL restrictApplicationUsage; + +/** + * Defines how often the BITAuthenticator checks if the user is allowed + * to run this application + * + * This requires `restrictApplicationUsage` to be enabled. + * + * _Default_: `BITAuthenticatorAppRestrictionEnforcementOnFirstLaunch` + * + * @see BITAuthenticatorAppRestrictionEnforcementFrequency + * @see restrictApplicationUsage + */ +@property (nonatomic, assign) BITAuthenticatorAppRestrictionEnforcementFrequency restrictionEnforcementFrequency; + +/** + * The authentication secret from HockeyApp. To find the right secret, + * click on your app on the HockeyApp dashboard, then on Show next to + * "Secret:". + * + * This is only needed if `identificationType` is set to `BITAuthenticatorIdentificationTypeHockeyAppEmail` + * + * @see identificationType + */ +@property (nonatomic, copy) NSString *authenticationSecret; + + +#pragma mark - Device based identification + +///----------------------------------------------------------------------------- +/// @name Device based identification +///----------------------------------------------------------------------------- + + +/** + * The baseURL of the webpage the user is redirected to if `identificationType` is + * set to `BITAuthenticatorIdentificationTypeDevice`; defaults to https://rink.hockeyapp.net. + * + * @see identificationType + */ +@property (nonatomic, strong) NSURL *webpageURL; + +/** + * URL to query the device's id via external webpage + * Built with the baseURL set in `webpageURL`. + */ +- (NSURL*) deviceAuthenticationURL; + +/** + * The url-scheme used to identify via `BITAuthenticatorIdentificationTypeDevice` + * + * Please make sure that the URL scheme is unique and not shared with other apps. + * + * If set to nil, the default scheme is used which is `ha`. + * + * @see identificationType + * @see handleOpenURL:sourceApplication:annotation: + */ +@property (nonatomic, copy) NSString *urlScheme; + +/** + Should be used by the app-delegate to forward handle application:openURL:sourceApplication:annotation: calls. + + This is required if `identificationType` is set to `BITAuthenticatorIdentificationTypeDevice`. + Your app needs to implement the default `ha` URL scheme or register its own scheme + via `urlScheme`. + BITAuthenticator checks if the given URL is actually meant to be parsed by it and will + return NO if it doesn't think so. It does this by checking the 'host'-part of the URL to be 'authorize', as well + as checking the protocol part. + Please make sure that if you're using a custom URL scheme, it does _not_ conflict with BITAuthenticator's. + If BITAuthenticator thinks the URL was meant to be an authorization URL, but could not find a valid token, it will + reset the stored identification token and state. + + Sample usage (in AppDelegate): + + - (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { + if ([[BITHockeyManager sharedHockeyManager].authenticator handleOpenURL:url + sourceApplication:sourceApplication + annotation:annotation]) { + return YES; + } else { + //do your own URL handling, return appropriate value + } + return NO; + } + + @param url Param `url` that was passed to the app + @param sourceApplication Param `sourceApplication` that was passed to the app + @param annotation Param `annotation` that was passed to the app + + @return YES if the URL request was handled, NO if the URL could not be handled/identified. + + @see identificationType + @see urlScheme + */ +- (BOOL) handleOpenURL:(NSURL *) url + sourceApplication:(NSString *) sourceApplication + annotation:(id) annotation; + +#pragma mark - Authentication + +///----------------------------------------------------------------------------- +/// @name Authentication +///----------------------------------------------------------------------------- + +/** + * Invoked automatic identification and validation + * + * If the `BITAuthenticator` is in automatic mode this will initiate identifying + * the current user according to the type specified in `identificationType` and + * validate if the identified user is allowed to run this application. + * + * If the user is not yet identified it will present a modal view asking the user to + * provide the required information. + * + * If your app provides it's own startup modal screen, e.g. a guide or a login, then + * you might either call this method once that UI is fully presented or once + * the user e.g. did actually login already. + * + * @warning You need to call this method in your code even if automatic mode is enabled! + * + * @see identificationType + */ +- (void) authenticateInstallation; + +/** + * Identifies the user according to the type specified in `identificationType`. + * + * If the `BITAuthenticator` is in manual mode, it's your responsibility to call + * this method. Depending on the `identificationType`, this method + * might present a viewController to let the user enter his/her credentials. + * + * If the Authenticator is in auto-mode, this is called by the authenticator itself + * once needed. + * + * @see identificationType + * @see authenticateInstallation + * @see validateWithCompletion: + * + * @param completion Block being executed once identification completed. Be sure to properly dispatch code to the main queue if necessary. + */ +- (void) identifyWithCompletion:(void(^)(BOOL identified, NSError *error)) completion; + +/** + * Returns YES if this app is identified according to the setting in `identificationType`. + * + * Since the identification process is done asynchronously (contacting the server), + * you need to observe the value change via KVO. + * + * @see identificationType + */ +@property (nonatomic, assign, readonly, getter = isIdentified) BOOL identified; + +/** + * Validates if the identified user is allowed to run this application. This checks + * with the HockeyApp backend and calls the completion-block once completed. + * + * If the `BITAuthenticator` is in manual mode, it's your responsibility to call + * this method. If the application is not yet identified, validation is not possible + * and the completion-block is called with an error set. + * + * If the `BITAuthenticator` is in auto-mode, this is called by the authenticator itself + * once needed. + * + * @see identificationType + * @see authenticateInstallation + * @see identifyWithCompletion: + * + * @param completion Block being executed once validation completed. Be sure to properly dispatch code to the main queue if necessary. + */ +- (void) validateWithCompletion:(void(^)(BOOL validated, NSError *error)) completion; + +/** + * Indicates if this installation is validated. + */ +@property (nonatomic, assign, readonly, getter = isValidated) BOOL validated; + +/** + * Removes all previously stored authentication tokens, UDIDs, etc. + */ +- (void) cleanupInternalStorage; + +/** + * Returns different values depending on `identificationType`. This can be used + * by the application to identify the user. + * + * @see identificationType + */ +- (NSString*) publicInstallationIdentifier; +@end + +#pragma mark - Protocol + +/** + * `BITAuthenticator` protocol + */ +@protocol BITAuthenticatorDelegate + +@optional +/** + * If the authentication (or validation) needs to identify the user, + * this delegate method is called with the viewController that we'll present. + * + * @param authenticator `BITAuthenticator` object + * @param viewController `UIViewController` used to identify the user + * + */ +- (void) authenticator:(BITAuthenticator *)authenticator willShowAuthenticationController:(UIViewController*) viewController; +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITAuthenticator.m b/submodules/HockeySDK-iOS/Classes/BITAuthenticator.m new file mode 100644 index 0000000000..f197219992 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITAuthenticator.m @@ -0,0 +1,990 @@ +/* + * Author: Stephan Diederich + * + * Copyright (c) 2013-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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + +#import "HockeySDKPrivate.h" +#import "BITAuthenticator_Private.h" +#import "BITAuthenticationViewController.h" +#import "BITHockeyAppClient.h" +#import "BITHockeyHelper.h" +#import "BITHockeyHelper+Application.h" +#import "BITHockeyBaseManagerPrivate.h" + +#include + +static NSString *const kBITAuthenticatorUUIDKey = @"BITAuthenticatorUUIDKey"; +static NSString *const kBITAuthenticatorIdentifierKey = @"BITAuthenticatorIdentifierKey"; +static NSString *const kBITAuthenticatorIdentifierTypeKey = @"BITAuthenticatorIdentifierTypeKey"; +static NSString *const kBITAuthenticatorLastAuthenticatedVersionKey = @"BITAuthenticatorLastAuthenticatedVersionKey"; +static NSString *const kBITAuthenticatorUserEmailKey = @"BITAuthenticatorUserEmailKey"; + +//deprecated +static NSString *const kBITAuthenticatorAuthTokenKey = @"BITAuthenticatorAuthTokenKey"; +static NSString *const kBITAuthenticatorAuthTokenTypeKey = @"BITAuthenticatorAuthTokenTypeKey"; + +typedef unsigned int bit_uint32; +static unsigned char kBITPNGHeader[8] = {137, 80, 78, 71, 13, 10, 26, 10}; +static unsigned char kBITPNGEndChunk[4] = {0x49, 0x45, 0x4e, 0x44}; + + +@interface BITAuthenticator() + +@property (nonatomic, assign) BOOL isSetup; + +@property (nonatomic, strong) id appDidBecomeActiveObserver; +@property (nonatomic, strong) id appDidEnterBackgroundObserver; +@property (nonatomic, strong) UIViewController *authenticationController; + +@end + + +@implementation BITAuthenticator + +- (instancetype)initWithAppIdentifier:(NSString *)appIdentifier appEnvironment:(BITEnvironment)environment { + self = [super initWithAppIdentifier:appIdentifier appEnvironment:environment]; + if (self) { + _webpageURL = [NSURL URLWithString:@"https://rink.hockeyapp.net/"]; + + _identificationType = BITAuthenticatorIdentificationTypeAnonymous; + _isSetup = NO; + _restrictApplicationUsage = NO; + _restrictionEnforcementFrequency = BITAuthenticatorAppRestrictionEnforcementOnFirstLaunch; + } + return self; +} + +- (void)dealloc { + [self unregisterObservers]; +} + +#pragma mark - BITHockeyBaseManager overrides + +- (void)startManager { + //disabled in TestFlight and the AppStore + if (self.appEnvironment != BITEnvironmentOther) { return; } + + self.isSetup = YES; +} + +#pragma mark - + +- (void)authenticateInstallation { + //disabled in TestFlight and the AppStore + if (self.appEnvironment != BITEnvironmentOther) { return; } + + // make sure this is called after startManager so all modules are fully setup + if (!self.isSetup) { + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(authenticateInstallation) object:nil]; + [self performSelector:@selector(authenticateInstallation) withObject:nil afterDelay:0.1]; + } else { + switch ([BITHockeyHelper applicationState]) { + case BITApplicationStateActive: + [self authenticate]; + break; + case BITApplicationStateBackground: + case BITApplicationStateInactive: + case BITApplicationStateUnknown: + // do nothing, wait for active state + break; + } + } + [self registerObservers]; +} + +- (void)authenticate { + [self identifyWithCompletion:^(BOOL identified, NSError *error) { + if (identified) { + if ([self needsValidation]) { + [self validate]; + } else { + [self dismissAuthenticationControllerAnimated:YES completion:nil]; + } + } else { + BITHockeyLogError(@"Failed to identify. Error: %@", error); + } + }]; +} + +#pragma mark - Identification + +- (void)identifyWithCompletion:(void (^)(BOOL identified, NSError *))completion { + if (self.authenticationController) { + BITHockeyLogDebug(@"Authentication controller already visible. Ignoring identify request"); + if (completion) { completion(NO, nil); } + return; + } + //first check if the stored identification type matches the one currently configured + NSString *storedTypeString = [self stringValueFromKeychainForKey:kBITAuthenticatorIdentifierTypeKey]; + NSString *configuredTypeString = [self.class stringForIdentificationType:self.identificationType]; + if (storedTypeString && ![storedTypeString isEqualToString:configuredTypeString]) { + BITHockeyLogDebug(@"Identification type mismatch for stored auth-token. Resetting."); + [self storeInstallationIdentifier:nil withType:BITAuthenticatorIdentificationTypeAnonymous]; + } + + NSString *identification = [self installationIdentifier]; + + if (identification) { + self.identified = YES; + if (completion) { completion(YES, nil); } + return; + } + + [self processFullSizeImage]; + if (self.identified) { + if (completion) { completion(YES, nil); } + return; + } + + //it's not identified yet, do it now + BITAuthenticationViewController *viewController = nil; + switch (self.identificationType) { + case BITAuthenticatorIdentificationTypeAnonymous: + [self storeInstallationIdentifier:bit_UUID() withType:BITAuthenticatorIdentificationTypeAnonymous]; + self.identified = YES; + if (completion) { completion(YES, nil); } + return; + case BITAuthenticatorIdentificationTypeHockeyAppUser: + viewController = [[BITAuthenticationViewController alloc] initWithDelegate:self]; + viewController.requirePassword = YES; + viewController.tableViewTitle = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerDataEmailAndPasswordDescription"); + break; + case BITAuthenticatorIdentificationTypeDevice: + viewController = [[BITAuthenticationViewController alloc] initWithDelegate:self]; + viewController.requirePassword = NO; + viewController.showsLoginViaWebButton = YES; + viewController.tableViewTitle = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerWebUDIDLoginDescription"); + break; + case BITAuthenticatorIdentificationTypeWebAuth: + viewController = [[BITAuthenticationViewController alloc] initWithDelegate:self]; + viewController.requirePassword = NO; + viewController.showsLoginViaWebButton = YES; + viewController.tableViewTitle = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerWebAuthLoginDescription"); + break; + case BITAuthenticatorIdentificationTypeHockeyAppEmail: + if (nil == self.authenticationSecret) { + NSError *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorAuthorizationSecretMissing + userInfo:@{NSLocalizedDescriptionKey:@"For email identification, the authentication secret must be set"}]; + if (completion) { completion(NO, error); } + return; + } + viewController = [[BITAuthenticationViewController alloc] initWithDelegate:self]; + viewController.requirePassword = NO; + viewController.tableViewTitle = BITHockeyLocalizedString(@"HockeyAuthenticationViewControllerDataEmailDescription"); + break; + } + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(authenticator:willShowAuthenticationController:)]) { + [strongDelegate authenticator:self willShowAuthenticationController:viewController]; + } + + NSAssert(viewController, @"ViewController should've been created"); + + viewController.email = [self stringValueFromKeychainForKey:kBITAuthenticatorUserEmailKey]; + self.authenticationController = viewController; + self.identificationCompletion = completion; + dispatch_async(dispatch_get_main_queue(), ^{ + [self showView:viewController]; + }); +} + +#pragma mark - Validation + +- (BOOL)needsValidation { + if (BITAuthenticatorIdentificationTypeAnonymous == self.identificationType) { + return NO; + } + if (NO == self.restrictApplicationUsage) { + return NO; + } + if (self.restrictionEnforcementFrequency == BITAuthenticatorAppRestrictionEnforcementOnFirstLaunch && + ![self.executableUUID isEqualToString:self.lastAuthenticatedVersion]) { + return YES; + } + if (NO == self.isValidated && self.restrictionEnforcementFrequency == BITAuthenticatorAppRestrictionEnforcementOnAppActive) { + return YES; + } + return NO; +} + +- (void)validate { + [self validateWithCompletion:^(BOOL validated, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (validated) { + [self dismissAuthenticationControllerAnimated:YES completion:nil]; + } else { + BITHockeyLogError(@"Validation failed with error: %@", error); + __weak typeof(self) weakSelf = self; + + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil + message:error.localizedDescription + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *okAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyOK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + [strongSelf validate]; + }]; + + [alertController addAction:okAction]; + [self showAlertController:alertController]; + } + }); + }]; +} + +- (void)validateWithCompletion:(void (^)(BOOL validated, NSError *))completion { + BOOL requirementsFulfilled = YES; + NSError *error = nil; + switch (self.identificationType) { + case BITAuthenticatorIdentificationTypeAnonymous: { + error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorErrorUnknown + userInfo:@{NSLocalizedDescriptionKey:@"Anonymous users can't be validated"}]; + requirementsFulfilled = NO; + break; + } + case BITAuthenticatorIdentificationTypeHockeyAppEmail: + if (nil == self.authenticationSecret) { + error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorAuthorizationSecretMissing + userInfo:@{NSLocalizedDescriptionKey:@"For email validation, the authentication secret must be set"}]; + requirementsFulfilled = NO; + break; + } + //no break + case BITAuthenticatorIdentificationTypeDevice: + case BITAuthenticatorIdentificationTypeHockeyAppUser: + case BITAuthenticatorIdentificationTypeWebAuth: + if (nil == self.installationIdentifier) { + error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorNotIdentified + userInfo:@{NSLocalizedDescriptionKey:@"Make sure to identify the installation first"}]; + requirementsFulfilled = NO; + } + break; + } + if (NO == requirementsFulfilled) { + if (completion) { + completion(NO, error); + } + return; + } + + NSString *validationPath = [NSString stringWithFormat:@"api/3/apps/%@/identity/validate", self.encodedAppIdentifier]; + + __weak typeof(self) weakSelf = self; + NSURLRequest *request = [self.hockeyAppClient requestWithMethod:@"GET" path:validationPath parameters:[self validationParameters]]; + + NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + __block NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; + + NSURLSessionDataTask *task = [session dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse __unused *response, NSError *innerError) { + typeof(self) strongSelf = weakSelf; + + [session finishTasksAndInvalidate]; + + [strongSelf handleValidationResponseWithData:data error:innerError completion:completion]; + }]; + [task resume]; +} + +- (void)handleValidationResponseWithData:(NSData *)responseData error:(NSError *)error completion:(void (^)(BOOL validated, NSError *))completion { + if (nil == responseData) { + NSDictionary *userInfo = @{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}; + if (error) { + NSMutableDictionary *dict = [userInfo mutableCopy]; + dict[NSUnderlyingErrorKey] = error; + userInfo = dict; + } + NSError *localError = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorNetworkError + userInfo:userInfo]; + self.validated = NO; + if (completion) { completion(NO, localError); } + } else { + NSError *validationParseError = nil; + BOOL valid = [self.class isValidationResponseValid:responseData error:&validationParseError]; + self.validated = valid; + if (valid) { + [self setLastAuthenticatedVersion:self.executableUUID]; + } + if (completion) { completion(valid, validationParseError); } + } +} + +- (NSDictionary *)validationParameters { + NSParameterAssert(self.installationIdentifier); + NSParameterAssert(self.installationIdentifierParameterString); + + NSString *installString = bit_appAnonID(NO); + if (installString) { + return @{self.installationIdentifierParameterString:self.installationIdentifier, @"install_string":installString}; + } + + return @{self.installationIdentifierParameterString:self.installationIdentifier}; +} + ++ (BOOL)isValidationResponseValid:(id)response error:(NSError *__autoreleasing *)error { + NSParameterAssert(response); + + NSError *jsonParseError = nil; + id jsonObject = [NSJSONSerialization JSONObjectWithData:response + options:0 + error:&jsonParseError]; + if (nil == jsonObject) { + if (error) { + *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorAPIServerReturnedInvalidResponse + userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}]; + } + return NO; + } + if (![jsonObject isKindOfClass:[NSDictionary class]]) { + if (error) { + *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorAPIServerReturnedInvalidResponse + userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}]; + } + return NO; + } + + NSString *status = jsonObject[@"status"]; + if ([status isEqualToString:@"not authorized"]) { + if (error) { + *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorNotAuthorized + userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationNotMember")}]; + } + return NO; + } else if ([status isEqualToString:@"not found"]) { + if (error) { + *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorUnknownApplicationID + userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationContactDeveloper")}]; + } + return NO; + } else if ([status isEqualToString:@"validated"]) { + return YES; + } else { + if (error) { + *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorAPIServerReturnedInvalidResponse + userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}]; + } + return NO; + } +} + +#pragma mark - AuthenticationViewController Helper + +/** + * This method has to be called on the main queue + */ +- (void)dismissAuthenticationControllerAnimated:(BOOL)animated completion:(void (^)(void))completion { + if (!self.authenticationController) { return; } + UIViewController *presentingViewController = [self.authenticationController presentingViewController]; + + // If there is no presenting view controller just remove view + if (presentingViewController) { + [self.authenticationController dismissViewControllerAnimated:animated completion:completion]; + } else { + [self.authenticationController.navigationController.view removeFromSuperview]; + if (completion) { + completion(); + } + } + self.authenticationController = nil; +} + +#pragma mark - AuthenticationViewControllerDelegate + +- (void)authenticationViewController:(UIViewController *)viewController + handleAuthenticationWithEmail:(NSString *)email + password:(NSString *)password + completion:(void (^)(BOOL, NSError *))completion { + + NSParameterAssert(email && email.length); + NSParameterAssert(self.identificationType == BITAuthenticatorIdentificationTypeHockeyAppEmail || (password && password.length)); + + // Trim whitespace from email in case the user has added a whitespace at the end of the email address. This shouldn't + // happen if devs use our UI but we've had 1-2 support tickets where the email contained whitespace at the end and + // verification failed because of that. + email = [email stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + NSURLRequest *request = [self requestForAuthenticationEmail:email password:password]; + + [self authenticationViewController:viewController handleAuthenticationWithEmail:email request:request completion:completion]; +} + +- (void)authenticationViewController:(UIViewController *) __unused viewController + handleAuthenticationWithEmail:(NSString *)email + request:(NSURLRequest *)request + completion:(void (^)(BOOL, NSError *))completion { + + __weak typeof(self) weakSelf = self; + NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + __block NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; + + NSURLSessionDataTask *task = [session dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError __unused *error) { + typeof(self) strongSelf = weakSelf; + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + + [session finishTasksAndInvalidate]; + + [strongSelf handleAuthenticationWithResponse:httpResponse email:email data:data completion:completion]; + }]; + [task resume]; +} + +- (void)authenticationViewControllerDidTapWebButton:(UIViewController *) __unused viewController { + NSURL *url = [self deviceAuthenticationURL]; + if (url) { + [[UIApplication sharedApplication] openURL:url]; + } +} + +#pragma mark - Networking + +- (void)handleAuthenticationWithResponse:(NSHTTPURLResponse *)response email:(NSString *)email data:(NSData *)data completion:(void (^)(BOOL, NSError *))completion { + NSError *authParseError = nil; + NSString *authToken = [self.class authenticationTokenFromURLResponse:response + data:data + error:&authParseError]; + BOOL identified; + if (authToken) { + identified = YES; + [self storeInstallationIdentifier:authToken withType:self.identificationType]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self dismissAuthenticationControllerAnimated:YES completion:nil]; + }); + BOOL success = [self addStringValueToKeychain:email forKey:kBITAuthenticatorUserEmailKey]; + if (!success) { + [self alertOnFailureStoringTokenInKeychain]; + } + } else { + identified = NO; + } + self.identified = identified; + if (completion) { completion(identified, authParseError); } + if (self.identificationCompletion) { + self.identificationCompletion(identified, authParseError); + self.identificationCompletion = nil; + } +} + +- (NSURLRequest *)requestForAuthenticationEmail:(NSString *)email password:(NSString *)password { + NSString *authenticationPath = [self authenticationPath]; + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + + NSString *installString = bit_appAnonID(NO); + if (installString) { + params[@"install_string"] = installString; + } + + if (BITAuthenticatorIdentificationTypeHockeyAppEmail == self.identificationType) { + NSString *authCode = BITHockeyMD5([NSString stringWithFormat:@"%@%@", + self.authenticationSecret ?: @"", + email ?: @""]); + + params[@"email"] = email ?: @""; + params[@"authcode"] = authCode.lowercaseString; + } + + NSMutableURLRequest *request = [self.hockeyAppClient requestWithMethod:@"POST" + path:authenticationPath + parameters:params]; + if (BITAuthenticatorIdentificationTypeHockeyAppUser == self.identificationType) { + NSString *authStr = [NSString stringWithFormat:@"%@:%@", email, password]; + NSData *authData = [authStr dataUsingEncoding:NSUTF8StringEncoding]; + NSString *authValue = [NSString stringWithFormat:@"Basic %@", [authData base64EncodedStringWithOptions:0]]; + [request setValue:authValue forHTTPHeaderField:@"Authorization"]; + } + + return request; +} + +- (NSString *)authenticationPath { + if (BITAuthenticatorIdentificationTypeHockeyAppUser == self.identificationType) { + return [NSString stringWithFormat:@"api/3/apps/%@/identity/authorize", self.encodedAppIdentifier]; + } else { + return [NSString stringWithFormat:@"api/3/apps/%@/identity/check", self.encodedAppIdentifier]; + } +} + ++ (NSString *)authenticationTokenFromURLResponse:(NSHTTPURLResponse *)urlResponse data:(NSData *)data error:(NSError *__autoreleasing *)error { + if (nil == urlResponse) { + if (error) { + *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorAPIServerReturnedInvalidResponse + userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}]; + } + return nil; + } + + switch (urlResponse.statusCode) { + case 401: + if (error) { + *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorNotAuthorized + userInfo:@{ + NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationWrongEmailPassword") + }]; + } + break; + case 200: + case 404: + //Do nothing, handled below + break; + default: + if (error) { + *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorAPIServerReturnedInvalidResponse + userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}]; + + } + break; + } + if (200 != urlResponse.statusCode && 404 != urlResponse.statusCode) { + //make sure we have an error created if user wanted to have one + NSParameterAssert(nil == error || *error); + return nil; + } + + NSError *jsonParseError = nil; + id jsonObject = [NSJSONSerialization JSONObjectWithData:data + options:0 + error:&jsonParseError]; + //no json or unexpected json + if (nil == jsonObject || ![jsonObject isKindOfClass:[NSDictionary class]]) { + if (error) { + NSDictionary *userInfo = @{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}; + if (jsonParseError) { + NSMutableDictionary *userInfoMutable = [userInfo mutableCopy]; + userInfoMutable[NSUnderlyingErrorKey] = jsonParseError; + userInfo = userInfoMutable; + } + *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorAPIServerReturnedInvalidResponse + userInfo:userInfo]; + } + return nil; + } + + NSString *status = jsonObject[@"status"]; + NSString *authToken = nil; + if ([status isEqualToString:@"identified"]) { + authToken = jsonObject[@"iuid"]; + } else if ([status isEqualToString:@"authorized"]) { + authToken = jsonObject[@"auid"]; + } else if ([status isEqualToString:@"not authorized"]) { + if (error) { + *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorNotAuthorized + userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationNotMember")}]; + + } + } + //if no error is set yet, but error parameter is given, return a generic error + if (nil == authToken && error && nil == *error) { + *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorAPIServerReturnedInvalidResponse + userInfo:@{NSLocalizedDescriptionKey:BITHockeyLocalizedString(@"HockeyAuthenticationFailedAuthenticate")}]; + } + return authToken; +} + +- (NSURL *)deviceAuthenticationURL { + NSString *whatParameter = nil; + switch (self.identificationType) { + case BITAuthenticatorIdentificationTypeWebAuth: + whatParameter = @"email"; + break; + case BITAuthenticatorIdentificationTypeDevice: + whatParameter = @"udid"; + break; + case BITAuthenticatorIdentificationTypeAnonymous: + case BITAuthenticatorIdentificationTypeHockeyAppEmail: + case BITAuthenticatorIdentificationTypeHockeyAppUser: + return nil; + } + NSURL *url = [self.webpageURL URLByAppendingPathComponent:[NSString stringWithFormat:@"apps/%@/authorize", self.encodedAppIdentifier]]; + NSParameterAssert(whatParameter && url.absoluteString); + url = [NSURL URLWithString:[NSString stringWithFormat:@"%@?what=%@", url.absoluteString, whatParameter]]; + return url; +} + +- (BOOL)handleOpenURL:(NSURL *)url + sourceApplication:(NSString *) __unused sourceApplication + annotation:(id)annotation { + //check if this URL was meant for us, if not return NO so the user can + //handle it + NSString *const kAuthorizationHost = @"authorize"; + NSString *urlScheme = self.urlScheme ?: [NSString stringWithFormat:@"ha%@", self.appIdentifier]; + if (!([[url scheme] isEqualToString:urlScheme] && [[url host] isEqualToString:kAuthorizationHost])) { + BITHockeyLogWarning(@"WARNING: URL scheme for authentication doesn't match!"); + return NO; + } + + NSString *installationIdentifier = nil; + NSString *localizedErrorDescription = nil; + switch (self.identificationType) { + case BITAuthenticatorIdentificationTypeWebAuth: { + NSString *email = nil; + [self.class email:&email andIUID:&installationIdentifier fromOpenURL:url]; + if (email) { + BOOL success = [self addStringValueToKeychain:email forKey:kBITAuthenticatorUserEmailKey]; + if (!success) { + [self alertOnFailureStoringTokenInKeychain]; + } + } else { + BITHockeyLogDebug(@"No email found in URL: %@", url); + } + localizedErrorDescription = @"Failed to retrieve parameters from URL."; + break; + } + case BITAuthenticatorIdentificationTypeDevice: { + installationIdentifier = [self.class UDIDFromOpenURL:url annotation:annotation]; + localizedErrorDescription = @"Failed to retrieve UDID from URL."; + break; + } + case BITAuthenticatorIdentificationTypeHockeyAppEmail: + case BITAuthenticatorIdentificationTypeAnonymous: + case BITAuthenticatorIdentificationTypeHockeyAppUser: + return NO; + } + + if (installationIdentifier) { + BITHockeyLogDebug(@"Authentication succeeded."); + if (NO == self.restrictApplicationUsage) { + [self dismissAuthenticationControllerAnimated:YES completion:nil]; + } + [self storeInstallationIdentifier:installationIdentifier withType:self.identificationType]; + self.identified = YES; + if (self.identificationCompletion) { + self.identificationCompletion(YES, nil); + self.identificationCompletion = nil; + } + } else { + //reset token + BITHockeyLogDebug(@"Resetting authentication token"); + [self storeInstallationIdentifier:nil withType:self.identificationType]; + self.identified = NO; + if (self.identificationCompletion) { + NSError *error = [NSError errorWithDomain:kBITAuthenticatorErrorDomain + code:BITAuthenticatorErrorUnknown + userInfo:@{NSLocalizedDescriptionKey:localizedErrorDescription}]; + self.identificationCompletion(NO, error); + self.identificationCompletion = nil; + } + } + return YES; +} + ++ (NSString *)UDIDFromOpenURL:(NSURL *)url annotation:(id) __unused annotation { + NSString *query = [url query]; + NSString *udid = nil; + //there should actually only one + static NSString *const UDIDQuerySpecifier = @"udid"; + for (NSString *queryComponents in [query componentsSeparatedByString:@"&"]) { + NSArray *parameterComponents = [queryComponents componentsSeparatedByString:@"="]; + if (2 == parameterComponents.count && [parameterComponents[0] isEqualToString:UDIDQuerySpecifier]) { + udid = parameterComponents[1]; + break; + } + } + return udid; +} + ++ (void)email:(NSString *__autoreleasing *)email andIUID:(NSString *__autoreleasing *)iuid fromOpenURL:(NSURL *)url { + NSString *query = [url query]; + //there should actually only one + static NSString *const EmailQuerySpecifier = @"email"; + static NSString *const IUIDQuerySpecifier = @"iuid"; + for (NSString *queryComponents in [query componentsSeparatedByString:@"&"]) { + NSArray *parameterComponents = [queryComponents componentsSeparatedByString:@"="]; + if (email && 2 == parameterComponents.count && [parameterComponents[0] isEqualToString:EmailQuerySpecifier]) { + *email = parameterComponents[1]; + } else if (iuid && 2 == parameterComponents.count && [parameterComponents[0] isEqualToString:IUIDQuerySpecifier]) { + *iuid = parameterComponents[1]; + } + } +} + +#pragma mark - Private helpers + +- (void)alertOnFailureStoringTokenInKeychain { + if ([BITHockeyHelper applicationState] != BITApplicationStateActive) { + return; + } + + BITHockeyLogError(@"[HockeySDK] ERROR: The authentication token could not be stored due to a keychain error. This is most likely a signing or keychain entitlement issue!"); +} + +- (void)cleanupInternalStorage { + [self removeKeyFromKeychain:kBITAuthenticatorIdentifierTypeKey]; + [self removeKeyFromKeychain:kBITAuthenticatorIdentifierKey]; + [self removeKeyFromKeychain:kBITAuthenticatorUUIDKey]; + [self removeKeyFromKeychain:kBITAuthenticatorUserEmailKey]; + [self setLastAuthenticatedVersion:nil]; + self.identified = NO; + self.validated = NO; + + //cleanup values stored from 3.5 Beta1..Beta3 + [self removeKeyFromKeychain:kBITAuthenticatorAuthTokenKey]; + [self removeKeyFromKeychain:kBITAuthenticatorAuthTokenTypeKey]; +} + +- (void)processFullSizeImage { +#ifdef BIT_INTERNAL_DEBUG + NSString* path = [[NSBundle mainBundle] pathForResource:@"iTunesArtwork" ofType:@"png"]; +#else + NSString *path = [[[NSBundle mainBundle] bundlePath] stringByAppendingString:@"/../iTunesArtwork"]; +#endif + + struct stat fs; + int fd = open([path UTF8String], O_RDONLY, 0); + if (fstat(fd, &fs) < 0) { + // File not found + close(fd); + return; + } + + BITHockeyLogDebug(@"Processing full size image for possible authentication"); + + unsigned char *buffer, *source; + source = (unsigned char *)malloc((unsigned long)fs.st_size); + if (read(fd, source, (unsigned long)fs.st_size) != fs.st_size) { + close(fd); + // Couldn't read file + free(source); + return; + } + + if ((fs.st_size < 20) || (memcmp(source, kBITPNGHeader, 8))) { + // Not a PNG + free(source); + return; + } + + buffer = source + 8; + + NSString *result = nil; + bit_uint32 length; + unsigned char *name; + unsigned char *data; + int chunk_index = 0; + long long bytes_left = fs.st_size - 8; + do { + memcpy(&length, buffer, 4); + length = ntohl(length); + + buffer += 4; + name = (unsigned char *)malloc(5); + name[4] = 0; + memcpy(name, buffer, 4); + + buffer += 4; + data = (unsigned char *)malloc(length + 1); + + if (bytes_left >= length) { + memcpy(data, buffer, length); + + buffer += length; + buffer += 4; + if (!strcmp((const char *)name, "tEXt")) { + data[length] = 0; + NSString *key = [NSString stringWithCString:(char *)data encoding:NSUTF8StringEncoding]; + + if ([key isEqualToString:@"Data"]) { + result = [NSString stringWithCString:(char *)(data + key.length + 1) encoding:NSUTF8StringEncoding]; + } + } + + if (!memcmp(name, kBITPNGEndChunk, 4)) { + chunk_index = 128; + } + } + + free(data); + free(name); + + bytes_left -= (length + 3 * 4); + } while ((chunk_index++ < 128) && (bytes_left > 8)); + + free(source); + + if (result) { + BITHockeyLogDebug(@"Authenticating using full size image information: %@", result); + [self handleOpenURL:[NSURL URLWithString:result] sourceApplication:nil annotation:nil]; + } else { + BITHockeyLogDebug(@"No authentication information found"); + } +} + +#pragma mark - NSNotification + +- (void)registerObservers { + __weak typeof(self) weakSelf = self; + if (nil == self.appDidBecomeActiveObserver) { + self.appDidBecomeActiveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf applicationDidBecomeActive:note]; + }]; + } + if (nil == self.appDidEnterBackgroundObserver) { + self.appDidEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf applicationDidEnterBackground:note]; + }]; + } +} + +- (void)unregisterObservers { + if (self.appDidBecomeActiveObserver) { + [[NSNotificationCenter defaultCenter] removeObserver:self.appDidBecomeActiveObserver]; + self.appDidBecomeActiveObserver = nil; + } + if (self.appDidEnterBackgroundObserver) { + [[NSNotificationCenter defaultCenter] removeObserver:self.appDidEnterBackgroundObserver]; + self.appDidEnterBackgroundObserver = nil; + } +} + +#pragma mark - Property overrides + +- (void)storeInstallationIdentifier:(NSString *)installationIdentifier withType:(BITAuthenticatorIdentificationType)type { + if (nil == installationIdentifier) { + [self removeKeyFromKeychain:kBITAuthenticatorIdentifierKey]; + [self removeKeyFromKeychain:kBITAuthenticatorIdentifierTypeKey]; + } else { + BOOL success1 = [self addStringValueToKeychainForThisDeviceOnly:installationIdentifier + forKey:kBITAuthenticatorIdentifierKey]; + BOOL success2 = [self addStringValueToKeychainForThisDeviceOnly:[self.class stringForIdentificationType:type] + forKey:kBITAuthenticatorIdentifierTypeKey]; + if (!success1 || !success2) { + [self alertOnFailureStoringTokenInKeychain]; + } + } +} + +- (NSString *)installationIdentifier { + NSString *identifier = [self stringValueFromKeychainForKey:kBITAuthenticatorIdentifierKey]; + return identifier; +} + +- (void)setLastAuthenticatedVersion:(NSString *)lastAuthenticatedVersion { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + if (nil == lastAuthenticatedVersion) { + [defaults removeObjectForKey:kBITAuthenticatorLastAuthenticatedVersionKey]; + } else { + [defaults setObject:lastAuthenticatedVersion + forKey:kBITAuthenticatorLastAuthenticatedVersionKey]; + } +} + +- (NSString *)lastAuthenticatedVersion { + return [[NSUserDefaults standardUserDefaults] objectForKey:kBITAuthenticatorLastAuthenticatedVersionKey]; +} + +- (NSString *)installationIdentifierParameterString { + switch (self.identificationType) { + case BITAuthenticatorIdentificationTypeHockeyAppEmail: + case BITAuthenticatorIdentificationTypeWebAuth: + return @"iuid"; + case BITAuthenticatorIdentificationTypeHockeyAppUser: + return @"auid"; + case BITAuthenticatorIdentificationTypeDevice: + return @"udid"; + case BITAuthenticatorIdentificationTypeAnonymous: + return @"uuid"; + } +} + ++ (NSString *)stringForIdentificationType:(BITAuthenticatorIdentificationType)identificationType { + switch (identificationType) { + case BITAuthenticatorIdentificationTypeHockeyAppEmail: + return @"iuid"; + case BITAuthenticatorIdentificationTypeWebAuth: + return @"webAuth"; + case BITAuthenticatorIdentificationTypeHockeyAppUser: + return @"auid"; + case BITAuthenticatorIdentificationTypeDevice: + return @"udid"; + case BITAuthenticatorIdentificationTypeAnonymous: + return @"uuid"; + } +} + +- (void)setIdentificationType:(BITAuthenticatorIdentificationType)identificationType { + if (_identificationType != identificationType) { + _identificationType = identificationType; + self.identified = NO; + self.validated = NO; + } +} + +- (NSString *)publicInstallationIdentifier { + switch (self.identificationType) { + case BITAuthenticatorIdentificationTypeHockeyAppEmail: + case BITAuthenticatorIdentificationTypeHockeyAppUser: + case BITAuthenticatorIdentificationTypeWebAuth: + return [self stringValueFromKeychainForKey:kBITAuthenticatorUserEmailKey]; + case BITAuthenticatorIdentificationTypeAnonymous: + case BITAuthenticatorIdentificationTypeDevice: + return [self stringValueFromKeychainForKey:kBITAuthenticatorIdentifierKey]; + } +} + +#pragma mark - Application Lifecycle + +- (void)applicationDidBecomeActive:(NSNotification *) __unused note { + [self authenticate]; +} + +- (void)applicationDidEnterBackground:(NSNotification *) __unused note { + if (BITAuthenticatorAppRestrictionEnforcementOnAppActive == self.restrictionEnforcementFrequency) { + self.validated = NO; + } +} + +@end + +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ diff --git a/submodules/HockeySDK-iOS/Classes/BITAuthenticator_Private.h b/submodules/HockeySDK-iOS/Classes/BITAuthenticator_Private.h new file mode 100644 index 0000000000..b0a97ea5b8 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITAuthenticator_Private.h @@ -0,0 +1,105 @@ +/* + * Author: Stephan Diederich + * + * Copyright (c) 2013-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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + +#import "BITAuthenticationViewController.h" + +@class BITHockeyAppClient; + +@interface BITAuthenticator () + +/** + Delegate that can be used to do any last minute configurations on the + presented viewController. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see `[BITHockeyManager setDelegate:]` + @see BITAuthenticatorDelegate + */ +@property (nonatomic, weak) id delegate; + +/** + * must be set + */ +@property (nonatomic, strong) BITHockeyAppClient *hockeyAppClient; + +#pragma mark - +/** + * holds the identifier of the last version that was authenticated + * only used if validation is set BITAuthenticatorValidationTypeOnFirstLaunch + */ +@property (nonatomic, copy) NSString *lastAuthenticatedVersion; + +/** + * returns the type of the string stored in installationIdentifierParameterString + */ +@property (nonatomic, copy, readonly) NSString *installationIdentifierParameterString; + +/** + * returns the string used to identify this app against the HockeyApp backend. + */ +@property (nonatomic, copy, readonly) NSString *installationIdentifier; + +/** + * method registered as observer for applicationDidEnterBackground events + * + * @param note NSNotification + */ +- (void) applicationDidEnterBackground:(NSNotification*) note; + +/** + * method registered as observer for applicationsDidBecomeActive events + * + * @param note NSNotification + */ +- (void) applicationDidBecomeActive:(NSNotification*) note; + +@property (nonatomic, copy) void(^identificationCompletion)(BOOL identified, NSError* error); + +#pragma mark - Overrides +@property (nonatomic, assign, readwrite, getter = isIdentified) BOOL identified; +@property (nonatomic, assign, readwrite, getter = isValidated) BOOL validated; + +#pragma mark - Testing +- (void) storeInstallationIdentifier:(NSString*) identifier withType:(BITAuthenticatorIdentificationType) type; +- (void)validateWithCompletion:(void (^)(BOOL validated, NSError *))completion; +- (void)authenticationViewController:(UIViewController *)viewController + handleAuthenticationWithEmail:(NSString *)email + request:(NSURLRequest *)request + completion:(void (^)(BOOL, NSError *))completion; +- (BOOL) needsValidation; +- (void) authenticate; +@end + +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ diff --git a/submodules/HockeySDK-iOS/Classes/BITBase.h b/submodules/HockeySDK-iOS/Classes/BITBase.h new file mode 100644 index 0000000000..4cc3cdc0c8 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITBase.h @@ -0,0 +1,13 @@ +#import "BITTelemetryObject.h" +#import "BITTelemetryData.h" + +@interface BITBase : BITTelemetryData + +@property (nonatomic, copy) NSString *baseType; + +- (instancetype)initWithCoder:(NSCoder *)coder; + +- (void)encodeWithCoder:(NSCoder *)coder; + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITBase.m b/submodules/HockeySDK-iOS/Classes/BITBase.m new file mode 100644 index 0000000000..efb54b7e26 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITBase.m @@ -0,0 +1,34 @@ +#import "BITBase.h" + +/// Data contract class for type Base. +@implementation BITBase + +/// +/// Adds all members of this class to a dictionary +/// @returns dictionary to which the members of this class will be added. +/// +- (NSDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary].mutableCopy; + if (self.baseType != nil) { + [dict setObject:self.baseType forKey:@"baseType"]; + } + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if(self) { + _baseType = [coder decodeObjectForKey:@"self.baseType"]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.baseType forKey:@"self.baseType"]; +} + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITBlurImageAnnotation.h b/submodules/HockeySDK-iOS/Classes/BITBlurImageAnnotation.h new file mode 100644 index 0000000000..cb5a1f3ecc --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITBlurImageAnnotation.h @@ -0,0 +1,33 @@ +/* + * Author: Moritz Haarmann + * + * 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 "BITImageAnnotation.h" + +@interface BITBlurImageAnnotation : BITImageAnnotation + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITBlurImageAnnotation.m b/submodules/HockeySDK-iOS/Classes/BITBlurImageAnnotation.m new file mode 100644 index 0000000000..9b1fd3f941 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITBlurImageAnnotation.m @@ -0,0 +1,109 @@ +/* + * Author: Moritz Haarmann + * + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "BITBlurImageAnnotation.h" + +@interface BITBlurImageAnnotation() + +@property (nonatomic, strong) CALayer* imageLayer; +@property (nonatomic, strong) UIImage* scaledImage; +@property (nonatomic, strong) CALayer* selectedLayer; + + +@end + +@implementation BITBlurImageAnnotation + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.clipsToBounds = YES; + self.imageLayer = [CALayer layer]; + [self.layer addSublayer:self.imageLayer]; + + self.selectedLayer = [CALayer layer]; + [self.layer insertSublayer:self.selectedLayer above:self.imageLayer]; + + self.selectedLayer.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.5].CGColor; + self.selectedLayer.opacity = 0.6f; + self.clipsToBounds = YES; + } + return self; +} + +- (void)setSourceImage:(UIImage *)sourceImage { + CGSize size = CGSizeMake(sourceImage.size.width/30, sourceImage.size.height/30); + + UIGraphicsBeginImageContext(size); + + [sourceImage drawInRect:CGRectMake(0, 0, size.width, size.height)]; + self.scaledImage = UIGraphicsGetImageFromCurrentImageContext(); + self.imageLayer.shouldRasterize = YES; + self.imageLayer.rasterizationScale = 1; + self.imageLayer.magnificationFilter = kCAFilterNearest; + self.imageLayer.contents = (id)self.scaledImage.CGImage; + + UIGraphicsEndImageContext(); +} + +- (void)setSelected:(BOOL)selected { + super.selected = selected; + + if (selected){ + self.selectedLayer.opacity = 0.6f; + } else { + self.selectedLayer.opacity = 0.0f; + } +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + [CATransaction begin]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-qual" + [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; +#pragma clang diagnostic pop + self.imageLayer.frame = self.imageFrame; + self.imageLayer.masksToBounds = YES; + + self.selectedLayer.frame= self.bounds; + [CATransaction commit]; +} + +- (BOOL)resizable { + return YES; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITCategoryContainer.h b/submodules/HockeySDK-iOS/Classes/BITCategoryContainer.h new file mode 100644 index 0000000000..97b25740cd --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCategoryContainer.h @@ -0,0 +1,17 @@ +#import +#import "HockeySDKFeatureConfig.h" + +#if HOCKEYSDK_FEATURE_METRICS + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +@interface BITCategoryContainer : NSObject + ++ (void)activateCategory; + +@end + +NS_ASSUME_NONNULL_END + +#endif /* HOCKEYSDK_FEATURE_METRICS */ diff --git a/submodules/HockeySDK-iOS/Classes/BITCategoryContainer.m b/submodules/HockeySDK-iOS/Classes/BITCategoryContainer.m new file mode 100644 index 0000000000..81f23e97f2 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCategoryContainer.m @@ -0,0 +1,144 @@ +#import "BITCategoryContainer.h" +#import "HockeySDKFeatureConfig.h" +#import + +#if HOCKEYSDK_FEATURE_METRICS + +@implementation BITCategoryContainer + ++ (void)activateCategory { + +} + +@end + + +#pragma mark - GZIP library + + +// +// GZIP.m +// +// Version 1.0.3 +// +// Created by Nick Lockwood on 03/06/2012. +// Copyright (C) 2012 Charcoal Design +// +// Distributed under the permissive zlib License +// Get the latest version from here: +// +// https://github.com/nicklockwood/GZIP +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#import + +static const NSUInteger ChunkSize = 16384; + +@implementation NSData (BITGZIP) + +- (NSData *)bit_gzippedDataWithCompressionLevel:(float)level +{ + if ([self length]) + { + z_stream stream; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.avail_in = (uint)[self length]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-qual" + stream.next_in = (Bytef *)[self bytes]; +#pragma clang diagnostic pop + stream.total_out = 0; + stream.avail_out = 0; + + int compression = (level < 0.0f)? Z_DEFAULT_COMPRESSION: (int)(roundf(level * 9)); + if (deflateInit2(&stream, compression, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) == Z_OK) + { + NSMutableData *data = [NSMutableData dataWithLength:ChunkSize]; + while (stream.avail_out == 0) + { + if (stream.total_out >= [data length]) + { + data.length += ChunkSize; + } + stream.next_out = (uint8_t *)[data mutableBytes] + stream.total_out; + stream.avail_out = (uInt)([data length] - stream.total_out); + deflate(&stream, Z_FINISH); + } + deflateEnd(&stream); + data.length = stream.total_out; + return data; + } + } + return nil; +} + +- (NSData *)bit_gzippedData +{ + return [self bit_gzippedDataWithCompressionLevel:-1.0f]; +} + +- (NSData *)bit_gunzippedData +{ + if ([self length]) + { + z_stream stream; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.avail_in = (uint)[self length]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-qual" + stream.next_in = (Bytef *)[self bytes]; +#pragma clang diagnostic pop + stream.total_out = 0; + stream.avail_out = 0; + + NSMutableData *data = [NSMutableData dataWithLength:(NSUInteger)([self length] * 1.5)]; + if (inflateInit2(&stream, 47) == Z_OK) + { + int status = Z_OK; + while (status == Z_OK) + { + if (stream.total_out >= [data length]) + { + data.length += [self length] / 2; + } + stream.next_out = (uint8_t *)[data mutableBytes] + stream.total_out; + stream.avail_out = (uInt)([data length] - stream.total_out); + status = inflate (&stream, Z_SYNC_FLUSH); + } + if (inflateEnd(&stream) == Z_OK) + { + if (status == Z_STREAM_END) + { + data.length = stream.total_out; + return data; + } + } + } + } + return nil; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_METRICS */ diff --git a/submodules/HockeySDK-iOS/Classes/BITChannel.h b/submodules/HockeySDK-iOS/Classes/BITChannel.h new file mode 100644 index 0000000000..3e4e5cbe77 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITChannel.h @@ -0,0 +1,47 @@ +#import +#import "HockeySDKFeatureConfig.h" + +#if HOCKEYSDK_FEATURE_METRICS + +@class BITConfiguration; +@class BITTelemetryData; +@class BITTelemetryContext; +@class BITPersistence; + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +/** + * Buffer of telemtry events, will be written to disk. Make sure the buffer is used in a threadsafe way. + */ +FOUNDATION_EXPORT char *_Nullable BITTelemetryEventBuffer; + +/** + * Items get queued before they are persisted and sent out as a batch. This class managed the queue, and forwards the batch + * to the persistence layer once the max batch count has been reached. + */ +@interface BITChannel : NSObject + + +/** + * Initializes a new BITChannel instance. + * + * @param telemetryContext the context used to add context values to the metrics payload + * @param persistence the persistence used to save metrics after the queue gets flushed + * + * @return the telemetry context + */ +- (instancetype)initWithTelemetryContext:(BITTelemetryContext *)telemetryContext persistence:(BITPersistence *) persistence; + +/** + * Reset BITSafeJsonEventsString so we can start appending JSON dictionaries. + * + * @param item The telemetry object, which should be processed + */ +- (void)enqueueTelemetryItem:(BITTelemetryData *)item; + +@end + +NS_ASSUME_NONNULL_END + +#endif /* HOCKEYSDK_FEATURE_METRICS */ diff --git a/submodules/HockeySDK-iOS/Classes/BITChannel.m b/submodules/HockeySDK-iOS/Classes/BITChannel.m new file mode 100644 index 0000000000..af43b50b36 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITChannel.m @@ -0,0 +1,505 @@ +#import "HockeySDKFeatureConfig.h" + +#if HOCKEYSDK_FEATURE_METRICS + +#import "HockeySDKPrivate.h" +#import "BITHockeyManager.h" +#import "BITChannelPrivate.h" +#import "BITHockeyHelper.h" +#import "BITHockeyHelper+Application.h" +#import "BITTelemetryContext.h" +#import "BITTelemetryData.h" +#import "BITEnvelope.h" +#import "BITData.h" +#import "BITDevice.h" +#import "BITPersistencePrivate.h" +#import "BITSender.h" +#import + + +static char *const BITDataItemsOperationsQueue = "net.hockeyapp.senderQueue"; +char *BITTelemetryEventBuffer; + +NSString *const BITChannelBlockedNotification = @"BITChannelBlockedNotification"; + +static NSInteger const BITDefaultMaxBatchSize = 50; +static NSInteger const BITDefaultBatchInterval = 15; +static NSInteger const BITSchemaVersion = 2; + +static NSInteger const BITDebugMaxBatchSize = 5; +static NSInteger const BITDebugBatchInterval = 3; + +typedef _Atomic(char*) atomic_charptr; + +NS_ASSUME_NONNULL_BEGIN + +@interface BITChannel () + +@property (nonatomic, weak, nullable) id appDidEnterBackgroundObserver; + +@end + +@implementation BITChannel + +@synthesize persistence = _persistence; +@synthesize channelBlocked = _channelBlocked; + +#pragma mark - Initialisation + +- (instancetype)init { + if ((self = [super init])) { + bit_resetEventBuffer(&BITTelemetryEventBuffer); + _dataItemCount = 0; + if (bit_isDebuggerAttached()) { + _maxBatchSize = BITDebugMaxBatchSize; + _batchInterval = BITDebugBatchInterval; + } else { + _maxBatchSize = BITDefaultMaxBatchSize; + _batchInterval = BITDefaultBatchInterval; + } + dispatch_queue_t serialQueue = dispatch_queue_create(BITDataItemsOperationsQueue, DISPATCH_QUEUE_SERIAL); + _dataItemsOperations = serialQueue; + + [self registerObservers]; + } + return self; +} + +- (instancetype)initWithTelemetryContext:(BITTelemetryContext *)telemetryContext persistence:(BITPersistence *)persistence { + if ((self = [self init])) { + _telemetryContext = telemetryContext; + _persistence = persistence; + } + return self; +} + +- (void)dealloc { + [self unregisterObservers]; + [self invalidateTimer]; +} + +#pragma mark - Observers + +- (void) registerObservers { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + __weak typeof(self) weakSelf = self; + + if (nil == self.appDidEnterBackgroundObserver) { + void (^notificationBlock)(NSNotification *note) = ^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + if ([strongSelf timerIsRunning]) { + + /** + * From the documentation for applicationDidEnterBackground: + * It's likely any background tasks you start in applicationDidEnterBackground: will not run until after that method exits, + * you should request additional background execution time before starting those tasks. In other words, + * first call beginBackgroundTaskWithExpirationHandler: and then run the task on a dispatch queue or secondary thread. + */ + UIApplication *application = [UIApplication sharedApplication]; + [strongSelf persistDataItemQueueWithBackgroundTask: application]; + } + }; + self.appDidEnterBackgroundObserver = [center addObserverForName:UIApplicationDidEnterBackgroundNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:notificationBlock]; + } +} + +- (void) unregisterObservers { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + id appDidEnterBackgroundObserver = self.appDidEnterBackgroundObserver; + if (appDidEnterBackgroundObserver) { + [center removeObserver:appDidEnterBackgroundObserver]; + self.appDidEnterBackgroundObserver = nil; + } +} + +#pragma mark - Queue management + +- (BOOL)isQueueBusy { + if (!self.channelBlocked) { + BOOL persistenceBusy = ![self.persistence isFreeSpaceAvailable]; + if (persistenceBusy) { + self.channelBlocked = YES; + [self sendBlockingChannelNotification]; + } + } + return self.channelBlocked; +} + +- (void)persistDataItemQueue:(char **)eventBuffer { + [self invalidateTimer]; + + // Make sure string (which points to BITTelemetryEventBuffer) is not changed. + char *previousBuffer = NULL; + char *newEmptyString = strdup(""); + do { + previousBuffer = *eventBuffer; + + // This swaps pointers and makes sure eventBuffer now has the balue of newEmptyString. + if (atomic_compare_exchange_strong((atomic_charptr *)eventBuffer, &previousBuffer, newEmptyString)) { + @synchronized(self) { + self.dataItemCount = 0; + } + break; + } + } while(true); + + // Nothing to persist, freeing memory and existing. + if (!previousBuffer || strlen(previousBuffer) == 0) { + free(previousBuffer); + return; + } + + // Persist the data + NSData *bundle = [NSData dataWithBytes:previousBuffer length:strlen(previousBuffer)]; + [self.persistence persistBundle:bundle]; + free(previousBuffer); + + // Reset both, the async-signal-safe and item counter. + [self resetQueue]; +} + +- (void)persistDataItemQueueWithBackgroundTask:(UIApplication *)application { + __weak typeof(self) weakSelf = self; + dispatch_async(self.dataItemsOperations, ^{ + typeof(self) strongSelf = weakSelf; + [strongSelf persistDataItemQueue:&BITTelemetryEventBuffer]; + }); + [self createBackgroundTaskWhileDataIsSending:application withWaitingGroup:nil]; +} + +- (void)createBackgroundTaskWhileDataIsSending:(UIApplication *)application + withWaitingGroup:(nullable dispatch_group_t)group { + if (application == nil) { + return; + } + + // Queues needs for waiting consistently. + NSArray *queues = @[ + self.dataItemsOperations, // For enqueue + self.persistence.persistenceQueue, // For persist + dispatch_get_main_queue() // For notification + ]; + + // Tracking for sender activity. + // BITPersistenceSuccessNotification - start sending + // BITSenderFinishSendingDataNotification - finish sending + __block dispatch_group_t senderGroup = dispatch_group_create(); + __block NSInteger senderCounter = 0; + __block id persistenceSuccessObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:BITPersistenceSuccessNotification + object:nil + queue:nil + usingBlock:^(__unused NSNotification *notification) { + dispatch_group_enter(senderGroup); + senderCounter++; + if (persistenceSuccessObserver) { + [[NSNotificationCenter defaultCenter] removeObserver:persistenceSuccessObserver]; + persistenceSuccessObserver = nil; + } + }]; + __block id senderFinishSendingDataObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:BITSenderFinishSendingDataNotification + object:nil + queue:nil + usingBlock:^(__unused NSNotification *notification) { + if (senderCounter > 0) { + dispatch_group_leave(senderGroup); + senderCounter--; + } + if (senderFinishSendingDataObserver) { + [[NSNotificationCenter defaultCenter] removeObserver:senderFinishSendingDataObserver]; + senderFinishSendingDataObserver = nil; + } + }]; + + BITHockeyLogVerbose(@"BITChannel: Start background task"); + __block UIBackgroundTaskIdentifier backgroundTask = [application beginBackgroundTaskWithExpirationHandler:^{ + BITHockeyLogVerbose(@"BITChannel: Background task is expired"); + [application endBackgroundTask:backgroundTask]; + backgroundTask = UIBackgroundTaskInvalid; + }]; + __block NSUInteger i = 0; + __block __weak void (^weakWaitBlock)(void); + void (^waitBlock)(void); + weakWaitBlock = waitBlock = ^{ + if (i < queues.count) { + dispatch_queue_t queue = [queues objectAtIndex:i++]; + BITHockeyLogVerbose(@"BITChannel: Waiting queue: %@", [[NSString alloc] initWithUTF8String:dispatch_queue_get_label(queue)]); + dispatch_async(queue, weakWaitBlock); + } else { + BITHockeyLogVerbose(@"BITChannel: Waiting sender"); + dispatch_group_notify(senderGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (backgroundTask != UIBackgroundTaskInvalid) { + BITHockeyLogVerbose(@"BITChannel: Cancel background task"); + [application endBackgroundTask:backgroundTask]; + backgroundTask = UIBackgroundTaskInvalid; + } + }); + } + }; + if (group != nil) { + BITHockeyLogVerbose(@"BITChannel: Waiting group"); + dispatch_group_notify((dispatch_group_t)group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), waitBlock); + } else { + waitBlock(); + } +} + +// Resets the event buffer and count of events in the queue. +- (void)resetQueue { + @synchronized (self) { + bit_resetEventBuffer(&BITTelemetryEventBuffer); + self.dataItemCount = 0; + } +} + +#pragma mark - Adding to queue + +- (void)enqueueTelemetryItem:(BITTelemetryData *)item { + [self enqueueTelemetryItem:item completionHandler:nil]; +} + +- (void)enqueueTelemetryItem:(BITTelemetryData *)item completionHandler:(nullable void (^)(void))completionHandler { + if (!item) { + + // Item is nil: Do not enqueue item and abort operation. + BITHockeyLogWarning(@"WARNING: TelemetryItem was nil."); + if(completionHandler) { + completionHandler(); + } + return; + } + + // First assigning self to weakSelf and then assigning this to strongSelf in the block is not very intuitive, this + // blog post explains it very well: https://dhoerl.wordpress.com/2013/04/23/i-finally-figured-out-weakself-and-strongself/ + __weak typeof(self) weakSelf = self; + dispatch_async(self.dataItemsOperations, ^{ + + typeof(self) strongSelf = weakSelf; + if (strongSelf.isQueueBusy) { + + // Case 1: Channel is in blocked state: Trigger sender, start timer to check after again after a while and abort operation. + BITHockeyLogDebug(@"INFO: The channel is saturated. %@ was dropped.", item.debugDescription); + if (![strongSelf timerIsRunning]) { + [strongSelf startTimer]; + } + + if(completionHandler) { + completionHandler(); + } + + return; + } + + // Should be outside of @synchronized block! + BOOL applicationIsInBackground = ([BITHockeyHelper applicationState] == BITApplicationStateBackground); + + // Enqueue item. + @synchronized(self) { + NSDictionary *dict = [strongSelf dictionaryForTelemetryData:item]; + [strongSelf appendDictionaryToEventBuffer:dict]; + + // If the app is running in the background. + if (strongSelf.dataItemCount >= strongSelf.maxBatchSize || applicationIsInBackground) { + + // Case 2: Max batch count has been reached or the app is running in the background, so write queue to disk and delete all items. + [strongSelf persistDataItemQueue:&BITTelemetryEventBuffer]; + } else if (strongSelf.dataItemCount > 0) { + + // Case 3: It is the first item, let's start the timer. + if (![strongSelf timerIsRunning]) { + [strongSelf startTimer]; + } + } + + if(completionHandler) { + completionHandler(); + } + } + }); +} + +#pragma mark - Envelope telemerty items + +- (NSDictionary *)dictionaryForTelemetryData:(BITTelemetryData *) telemetryData { + + BITEnvelope *envelope = [self envelopeForTelemetryData:telemetryData]; + NSDictionary *dict = [envelope serializeToDictionary]; + return dict; +} + +- (BITEnvelope *)envelopeForTelemetryData:(BITTelemetryData *)telemetryData { + telemetryData.version = @(BITSchemaVersion); + + BITData *data = [BITData new]; + data.baseData = telemetryData; + data.baseType = telemetryData.dataTypeName; + + BITEnvelope *envelope = [BITEnvelope new]; + envelope.time = bit_utcDateString([NSDate date]); + envelope.iKey = self.telemetryContext.appIdentifier; + + envelope.tags = self.telemetryContext.contextDictionary; + envelope.data = data; + envelope.name = telemetryData.envelopeTypeName; + + return envelope; +} + +#pragma mark - Serialization Helper + +- (NSString *)serializeDictionaryToJSONString:(NSDictionary *)dictionary { + NSError *error; + NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:(NSJSONWritingOptions)0 error:&error]; + if (!data) { + BITHockeyLogError(@"ERROR: JSONSerialization error: %@", error.localizedDescription); + return @"{}"; + } else { + return (NSString *)[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + } +} + +#pragma mark JSON Stream + +- (void)appendDictionaryToEventBuffer:(NSDictionary *)dictionary { + if (dictionary) { + NSString *string = [self serializeDictionaryToJSONString:dictionary]; + + // Since we can't persist every event right away, we write it to a simple C string. + // This can then be written to disk by a signal handler in case of a crash. + @synchronized (self) { + bit_appendStringToEventBuffer(string, &BITTelemetryEventBuffer); + self.dataItemCount += 1; + } + + BITHockeyLogVerbose(@"VERBOSE: Appended data to buffer:\n%@", string); + } +} + +void bit_appendStringToEventBuffer(NSString *string, char **eventBuffer) { + if (eventBuffer == NULL) { + return; + } + + if (!string) { + return; + } + + if (*eventBuffer == NULL || strlen(*eventBuffer) == 0) { + bit_resetEventBuffer(eventBuffer); + } + + if (string.length == 0) { + return; + } + + do { + char *newBuffer = NULL; + char *previousBuffer = *eventBuffer; + + // Concatenate old string with new JSON string and add a comma. + asprintf(&newBuffer, "%s%.*s\n", previousBuffer, (int)MIN(string.length, (NSUInteger)INT_MAX), string.UTF8String); + + // Compare newBuffer and previousBuffer. If they point to the same address, we are safe to use them. + if (atomic_compare_exchange_strong((atomic_charptr *)eventBuffer, &previousBuffer, newBuffer)) { + + // Free the intermediate pointer. + free(previousBuffer); + return; + } else { + + // newBuffer has been changed by another thread. + free(newBuffer); + } + } while (true); +} + +void bit_resetEventBuffer(char **eventBuffer) { + if (!eventBuffer) { + return; + } + + char *prevString = NULL; + char *newEmptyString = strdup(""); + do { + prevString = *eventBuffer; + + // Compare pointers to strings to make sure we are still threadsafe! + if (atomic_compare_exchange_strong((atomic_charptr *)eventBuffer, &prevString, newEmptyString)) { + free(prevString); + return; + } + } while(true); +} + +#pragma mark - Batching + +- (NSUInteger)maxBatchSize { + if(_maxBatchSize <= 0){ + return BITDefaultMaxBatchSize; + } + return _maxBatchSize; +} + +- (void)invalidateTimer { + @synchronized(self) { + if (self.timerSource != nil) { + dispatch_source_cancel((dispatch_source_t)self.timerSource); + self.timerSource = nil; + } + } +} + +-(BOOL)timerIsRunning { + @synchronized(self) { + return self.timerSource != nil; + } +} + +- (void)startTimer { + @synchronized(self) { + + // Reset timer, if it is already running. + [self invalidateTimer]; + + dispatch_source_t timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.dataItemsOperations); + dispatch_source_set_timer(timerSource, dispatch_walltime(NULL, NSEC_PER_SEC * self.batchInterval), 1ull * NSEC_PER_SEC, 1ull * NSEC_PER_SEC); + __weak typeof(self) weakSelf = self; + dispatch_source_set_event_handler(timerSource, ^{ + typeof(self) strongSelf = weakSelf; + if (strongSelf) { + if (strongSelf.dataItemCount > 0) { + [strongSelf persistDataItemQueue:&BITTelemetryEventBuffer]; + } else { + strongSelf.channelBlocked = NO; + } + [strongSelf invalidateTimer]; + } + }); + dispatch_resume(timerSource); + self.timerSource = timerSource; + } +} + +/** + * Send a BITHockeyBlockingChannelNotification to the main thread to notify observers that channel can't enqueue new items. + * This is typically used to trigger sending. + */ +- (void)sendBlockingChannelNotification { + dispatch_async(dispatch_get_main_queue(), ^{ + BITHockeyLogDebug(@"Sending notification: %@", BITChannelBlockedNotification); + [[NSNotificationCenter defaultCenter] postNotificationName:BITChannelBlockedNotification + object:nil + userInfo:nil]; + }); +} + +@end + +NS_ASSUME_NONNULL_END + +#endif /* HOCKEYSDK_FEATURE_METRICS */ + diff --git a/submodules/HockeySDK-iOS/Classes/BITChannelPrivate.h b/submodules/HockeySDK-iOS/Classes/BITChannelPrivate.h new file mode 100644 index 0000000000..97c836f709 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITChannelPrivate.h @@ -0,0 +1,125 @@ +#import +#import +#import "HockeySDKFeatureConfig.h" + +#if HOCKEYSDK_FEATURE_METRICS + +@class BITTelemetryData; +@class BITTelemetryContext; +@class BITPersistence; + +#import "BITChannel.h" + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +@interface BITChannel () + +/** + * Notification that will be send on the main thread to notifiy observers that channel can't enqueue new items. + * This is typically used to trigger sending to the server. + */ +FOUNDATION_EXPORT NSString *const BITChannelBlockedNotification; + +/** + * Telemetry context used by the channel to create the payload (testing). + */ +@property (nonatomic, strong) BITTelemetryContext *telemetryContext; + +/** + * Persistence instance for storing files after the queue gets flushed (testing). + */ +@property (nonatomic, strong) BITPersistence *persistence; + +/* + * Threshold for sending data to the server. Default batch size for debugging is 150, for release + * configuration, the batch size is 5. + * + * Default: 50 + * + * @warning: We advice to not set the batch size below 5 events. + */ +@property (nonatomic) NSUInteger maxBatchSize; + +/* + * Interval for sending data to the server in seconds. + * + * Default: 15 + */ +@property (nonatomic, assign) NSInteger batchInterval; + +/** + * A timer source which is used to flush the queue after a cretain time. + */ +@property (nonatomic, strong, nullable) dispatch_source_t timerSource; + +/** + * A queue which makes array operations thread safe. + */ +@property (nonatomic, strong) dispatch_queue_t dataItemsOperations; + +/** + * An integer value that keeps tracks of the number of data items added to the JSON Stream string. + */ +@property (nonatomic, assign) NSUInteger dataItemCount; + +/** + * Indicates that channel is currently in a blocked state. + */ +@property BOOL channelBlocked; + +/** + * Manually trigger the BITChannel to persist all items currently in its data item queue. + */ +- (void)persistDataItemQueue:(char *_Nullable*_Nullable)eventBuffer; + +/** + * Create background task for queues and group. + */ +- (void)createBackgroundTaskWhileDataIsSending:(UIApplication *)application + withWaitingGroup:(nullable dispatch_group_t)group; + +/** + * Adds the specified dictionary to the JSON Stream string. + * + * @param dictionary the dictionary object which is to be added to the JSON Stream queue string. + */ +- (void)appendDictionaryToEventBuffer:(NSDictionary *)dictionary; + +/** + * A C function that serializes a given dictionary to JSON and appends it to a char string + * + * @param string The C string which the dictionary's JSON representation will be appended to. + */ +void bit_appendStringToEventBuffer(NSString *string, char *__nonnull*__nonnull eventBuffer); + +/** + * Reset the event buffer so we can start appending JSON dictionaries. + * + * @param eventBuffer The string that will be reset. + */ +void bit_resetEventBuffer(char *__nonnull*__nonnull eventBuffer); + +/** + * A method which indicates whether the telemetry pipeline is busy and no new data should be enqueued. + * Currently, we drop telemetry data if this returns YES. + * This depends on defaultMaxBatchCount and defaultBatchInterval. + * + * @return Returns yes if currently no new data should be enqueued on the channel. + */ +- (BOOL)isQueueBusy; + +/** + * Enqueue a telemetry item. This is for testing purposes where we actually use the completion handler. + * + * @param completionHandler The completion handler that will be called after enqueuing a BITTelemetryData object. + * + * @discussion intended for testing purposes. + */ +- (void)enqueueTelemetryItem:(BITTelemetryData *)item completionHandler:(nullable void (^)(void))completionHandler; + +@end + +NS_ASSUME_NONNULL_END + +#endif /* HOCKEYSDK_FEATURE_METRICS */ diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashAttachment.h b/submodules/HockeySDK-iOS/Classes/BITCrashAttachment.h new file mode 100644 index 0000000000..9f7d4273c0 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashAttachment.h @@ -0,0 +1,59 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 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 "BITHockeyAttachment.h" + +/** + Deprecated: Provides support to add binary attachments to crash reports + + This class is not needed any longer and exists for compatibility purposes with + HockeySDK-iOS 3.5.5. + + It is a subclass of `BITHockeyAttachment` which only provides an initializer + that is compatible with the one of HockeySDK-iOS 3.5.5. + + This is used by `[BITCrashManagerDelegate attachmentForCrashManager:]` + + @see BITHockeyAttachment + */ +@interface BITCrashAttachment : BITHockeyAttachment + +/** + Create an BITCrashAttachment instance with a given filename and NSData object + + @param filename The filename the attachment should get + @param crashAttachmentData The attachment data as NSData + @param contentType The content type of your data as MIME type + + @return An instance of BITCrashAttachment + */ +- (instancetype)initWithFilename:(NSString *)filename + crashAttachmentData:(NSData *)crashAttachmentData + contentType:(NSString *)contentType; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashAttachment.m b/submodules/HockeySDK-iOS/Classes/BITCrashAttachment.m new file mode 100644 index 0000000000..f4f408d25d --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashAttachment.m @@ -0,0 +1,48 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER + +#import "BITCrashAttachment.h" + +@implementation BITCrashAttachment + +- (instancetype)initWithFilename:(NSString *)filename + crashAttachmentData:(NSData *)crashAttachmentData + contentType:(NSString *)contentType +{ + self = [super initWithFilename:filename hockeyAttachmentData:crashAttachmentData contentType:contentType]; + + return self; +} + +@end + +#endif diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashCXXExceptionHandler.h b/submodules/HockeySDK-iOS/Classes/BITCrashCXXExceptionHandler.h new file mode 100644 index 0000000000..9efc2d2292 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashCXXExceptionHandler.h @@ -0,0 +1,49 @@ +/* + * Author: Gwynne Raskind + * + * Copyright (c) 2015 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 +#import "HockeySDKNullability.h" + +typedef struct { + const void * __nullable exception; + const char * __nullable exception_type_name; + const char * __nullable exception_message; + uint32_t exception_frames_count; + const uintptr_t * __nonnull exception_frames; +} BITCrashUncaughtCXXExceptionInfo; + +typedef void (*BITCrashUncaughtCXXExceptionHandler)( + const BITCrashUncaughtCXXExceptionInfo * __nonnull info +); + +@interface BITCrashUncaughtCXXExceptionHandlerManager : NSObject + ++ (void)addCXXExceptionHandler:(nonnull BITCrashUncaughtCXXExceptionHandler)handler; ++ (void)removeCXXExceptionHandler:(nonnull BITCrashUncaughtCXXExceptionHandler)handler; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashCXXExceptionHandler.mm b/submodules/HockeySDK-iOS/Classes/BITCrashCXXExceptionHandler.mm new file mode 100644 index 0000000000..233ab5ea27 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashCXXExceptionHandler.mm @@ -0,0 +1,247 @@ +/* + * Author: Gwynne Raskind + * + * Copyright (c) 2015 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER + +#import "BITCrashCXXExceptionHandler.h" +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +typedef std::vector BITCrashUncaughtCXXExceptionHandlerList; +typedef struct +{ + void *exception_object; + uintptr_t call_stack[128]; + uint32_t num_frames; +} BITCrashCXXExceptionTSInfo; + +static bool _BITCrashIsOurTerminateHandlerInstalled = false; +static std::terminate_handler _BITCrashOriginalTerminateHandler = nullptr; +static BITCrashUncaughtCXXExceptionHandlerList _BITCrashUncaughtExceptionHandlerList; +// We are ignoring warnings about OSSpinLock being deprecated because a replacement API +// for this was introduced only in iOS 10.0. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +static OSSpinLock _BITCrashCXXExceptionHandlingLock = OS_SPINLOCK_INIT; +#pragma clang diagnostic pop +static pthread_key_t _BITCrashCXXExceptionInfoTSDKey = 0; + +@implementation BITCrashUncaughtCXXExceptionHandlerManager + +extern "C" void __attribute__((noreturn)) __cxa_throw(void *exception_object, std::type_info *tinfo, void (*dest)(void *)) +{ + // Purposely do not take a lock in this function. The aim is to be as fast as + // possible. While we could really use some of the info set up by the real + // __cxa_throw, if we call through we never get control back - the function is + // noreturn and jumps to landing pads. Most of the stuff in __cxxabiv1 also + // won't work yet. We therefore have to do these checks by hand. + + // The technique for distinguishing Objective-C exceptions is based on the + // implementation of objc_exception_throw(). It's weird, but it's fast. The + // explicit symbol load and NULL checks should guard against the + // implementation changing in a future version. (Or not existing in an earlier + // version). + + typedef void (*cxa_throw_func)(void *, std::type_info *, void (*)(void *)) __attribute__((noreturn)); + static dispatch_once_t predicate = 0; + static cxa_throw_func __original__cxa_throw = nullptr; + static const void **__real_objc_ehtype_vtable = nullptr; + + dispatch_once(&predicate, ^ { + __original__cxa_throw = reinterpret_cast(dlsym(RTLD_NEXT, "__cxa_throw")); + __real_objc_ehtype_vtable = reinterpret_cast(dlsym(RTLD_DEFAULT, "objc_ehtype_vtable")); + }); + + // Actually check for Objective-C exceptions. + if (tinfo && __real_objc_ehtype_vtable && // Guard from an ABI change + *reinterpret_cast(tinfo) == __real_objc_ehtype_vtable + 2) { + goto callthrough; + } + + // Any other exception that came here has to be C++, since Objective-C is the + // only (known) runtime that hijacks the C++ ABI this way. We need to save off + // a backtrace. + // Invariant: If the terminate handler is installed, the TSD key must also be + // initialized. + if (_BITCrashIsOurTerminateHandlerInstalled) { + BITCrashCXXExceptionTSInfo *info = static_cast(pthread_getspecific(_BITCrashCXXExceptionInfoTSDKey)); + + if (!info) { + info = reinterpret_cast(calloc(1, sizeof(BITCrashCXXExceptionTSInfo))); + pthread_setspecific(_BITCrashCXXExceptionInfoTSDKey, info); + } + info->exception_object = exception_object; + // XXX: All significant time in this call is spent right here. + info->num_frames = backtrace(reinterpret_cast(&info->call_stack[0]), sizeof(info->call_stack) / sizeof(info->call_stack[0])); + } + +callthrough: + if (__original__cxa_throw) { + __original__cxa_throw(exception_object, tinfo, dest); + } else { + abort(); + } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunreachable-code" + __builtin_unreachable(); +#pragma clang diagnostic pop +} + +__attribute__((always_inline)) +static inline void BITCrashIterateExceptionHandlers_unlocked(const BITCrashUncaughtCXXExceptionInfo &info) +{ + for (const auto &handler : _BITCrashUncaughtExceptionHandlerList) { + handler(&info); + } +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +static void BITCrashUncaughtCXXTerminateHandler(void) +{ + BITCrashUncaughtCXXExceptionInfo info = { + .exception = nullptr, + .exception_type_name = nullptr, + .exception_message = nullptr, + .exception_frames_count = 0, + .exception_frames = nullptr, + }; + auto p = std::current_exception(); + + OSSpinLockLock(&_BITCrashCXXExceptionHandlingLock); { + if (p) { // explicit operator bool + info.exception = reinterpret_cast(&p); + info.exception_type_name = __cxxabiv1::__cxa_current_exception_type()->name(); + + BITCrashCXXExceptionTSInfo *recorded_info = reinterpret_cast(pthread_getspecific(_BITCrashCXXExceptionInfoTSDKey)); + + if (recorded_info) { + info.exception_frames_count = recorded_info->num_frames - 1; + info.exception_frames = &recorded_info->call_stack[1]; + } else { + // There's no backtrace, grab this function's trace instead. Probably + // means the exception came from a dynamically loaded library. + void *frames[128] = { nullptr }; + + info.exception_frames_count = backtrace(&frames[0], sizeof(frames) / sizeof(frames[0])) - 1; + info.exception_frames = reinterpret_cast(&frames[1]); + } + + try { + std::rethrow_exception(p); + } catch (const std::exception &e) { // C++ exception. + info.exception_message = e.what(); + BITCrashIterateExceptionHandlers_unlocked(info); + } catch (const std::exception *e) { // C++ exception by pointer. + info.exception_message = e->what(); + BITCrashIterateExceptionHandlers_unlocked(info); + } catch (const std::string &e) { // C++ string as exception. + info.exception_message = e.c_str(); + BITCrashIterateExceptionHandlers_unlocked(info); + } catch (const std::string *e) { // C++ string pointer as exception. + info.exception_message = e->c_str(); + BITCrashIterateExceptionHandlers_unlocked(info); + } catch (const char *e) { // Plain string as exception. + info.exception_message = e; + BITCrashIterateExceptionHandlers_unlocked(info); + } catch (id __unused e) { // Objective-C exception. Pass it on to Foundation. + OSSpinLockUnlock(&_BITCrashCXXExceptionHandlingLock); + if (_BITCrashOriginalTerminateHandler != nullptr) { + _BITCrashOriginalTerminateHandler(); + } + return; + } catch (...) { // Any other kind of exception. No message. + BITCrashIterateExceptionHandlers_unlocked(info); + } + } + } OSSpinLockUnlock(&_BITCrashCXXExceptionHandlingLock); // In case terminate is called reentrantly by pasing it on + + if (_BITCrashOriginalTerminateHandler != nullptr) { + _BITCrashOriginalTerminateHandler(); + } else { + abort(); + } +} + ++ (void)addCXXExceptionHandler:(BITCrashUncaughtCXXExceptionHandler)handler +{ + static dispatch_once_t key_predicate = 0; + + // This only EVER has to be done once, since we don't delete the TSD later + // (there's no reason to delete it). + dispatch_once(&key_predicate, ^ { + pthread_key_create(&_BITCrashCXXExceptionInfoTSDKey, free); + }); + + OSSpinLockLock(&_BITCrashCXXExceptionHandlingLock); { + if (!_BITCrashIsOurTerminateHandlerInstalled) { + _BITCrashOriginalTerminateHandler = std::set_terminate(BITCrashUncaughtCXXTerminateHandler); + _BITCrashIsOurTerminateHandlerInstalled = true; + } + _BITCrashUncaughtExceptionHandlerList.push_back(handler); + } OSSpinLockUnlock(&_BITCrashCXXExceptionHandlingLock); +} + ++ (void)removeCXXExceptionHandler:(BITCrashUncaughtCXXExceptionHandler)handler +{ + OSSpinLockLock(&_BITCrashCXXExceptionHandlingLock); { + auto i = std::find(_BITCrashUncaughtExceptionHandlerList.begin(), _BITCrashUncaughtExceptionHandlerList.end(), handler); + + if (i != _BITCrashUncaughtExceptionHandlerList.end()) { + _BITCrashUncaughtExceptionHandlerList.erase(i); + } + + if (_BITCrashIsOurTerminateHandlerInstalled) { + if (_BITCrashUncaughtExceptionHandlerList.empty()) { + std::terminate_handler previous_handler = std::set_terminate(_BITCrashOriginalTerminateHandler); + + if (previous_handler != BITCrashUncaughtCXXTerminateHandler) { + std::set_terminate(previous_handler); + } else { + _BITCrashIsOurTerminateHandlerInstalled = false; + _BITCrashOriginalTerminateHandler = nullptr; + } + } + } + } OSSpinLockUnlock(&_BITCrashCXXExceptionHandlingLock); +} +#pragma clang diagnostic pop + +@end + +#endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashDetails.h b/submodules/HockeySDK-iOS/Classes/BITCrashDetails.h new file mode 100644 index 0000000000..be2d62fa3a --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashDetails.h @@ -0,0 +1,123 @@ +/* + * 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 + +/** + * Provides details about the crash that occurred in the previous app session + */ +@interface BITCrashDetails : NSObject + +/** + * UUID for the crash report + */ +@property (nonatomic, readonly, copy) NSString *incidentIdentifier; + +/** + * UUID for the app installation on the device + */ +@property (nonatomic, readonly, copy) NSString *reporterKey; + +/** + * Signal that caused the crash + */ +@property (nonatomic, readonly, copy) NSString *signal; + +/** + * Exception name that triggered the crash, nil if the crash was not caused by an exception + */ +@property (nonatomic, readonly, copy) NSString *exceptionName; + +/** + * Exception reason, nil if the crash was not caused by an exception + */ +@property (nonatomic, readonly, copy) NSString *exceptionReason; + +/** + * Date and time the app started, nil if unknown + */ +@property (nonatomic, readonly, strong) NSDate *appStartTime; + +/** + * Date and time the crash occurred, nil if unknown + */ +@property (nonatomic, readonly, strong) NSDate *crashTime; + +/** + * Operation System version string the app was running on when it crashed. + */ +@property (nonatomic, readonly, copy) NSString *osVersion; + +/** + * Operation System build string the app was running on when it crashed + * + * This may be unavailable. + */ +@property (nonatomic, readonly, copy) NSString *osBuild; + +/** + * CFBundleShortVersionString value of the app that crashed + * + * Can be `nil` if the crash was captured with an older version of the SDK + * or if the app doesn't set the value. + */ +@property (nonatomic, readonly, copy) NSString *appVersion; + +/** + * CFBundleVersion value of the app that crashed + */ +@property (nonatomic, readonly, copy) NSString *appBuild; + +/** + * Identifier of the app process that crashed + */ +@property (nonatomic, readonly, assign) NSUInteger appProcessIdentifier; + +/** + Indicates if the app was killed while being in foreground from the iOS + + If `[BITCrashManager enableAppNotTerminatingCleanlyDetection]` is enabled, use this on startup + to check if the app starts the first time after it was killed by iOS in the previous session. + + This can happen if it consumed too much memory or the watchdog killed the app because it + took too long to startup or blocks the main thread for too long, or other reasons. See Apple + documentation: https://developer.apple.com/library/ios/qa/qa1693/_index.html + + See `[BITCrashManager enableAppNotTerminatingCleanlyDetection]` for more details about which kind of kills can be detected. + + @warning This property only has a correct value, once `[BITHockeyManager startManager]` was + invoked! In addition, it is automatically disabled while a debugger session is active! + + @see `[BITCrashManager enableAppNotTerminatingCleanlyDetection]` + @see `[BITCrashManager didReceiveMemoryWarningInLastSession]` + + @return YES if the details represent an app kill instead of a crash + */ +- (BOOL)isAppKill; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashDetails.m b/submodules/HockeySDK-iOS/Classes/BITCrashDetails.m new file mode 100644 index 0000000000..b9fa38f6bb --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashDetails.m @@ -0,0 +1,81 @@ +/* + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER + +#import "BITCrashDetails.h" +#import "BITCrashDetailsPrivate.h" + +NSString *const kBITCrashKillSignal = @"SIGKILL"; + +@implementation BITCrashDetails + +- (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier + reporterKey:(NSString *)reporterKey + signal:(NSString *)signal + exceptionName:(NSString *)exceptionName + exceptionReason:(NSString *)exceptionReason + appStartTime:(NSDate *)appStartTime + crashTime:(NSDate *)crashTime + osVersion:(NSString *)osVersion + osBuild:(NSString *)osBuild + appVersion:(NSString *)appVersion + appBuild:(NSString *)appBuild + appProcessIdentifier:(NSUInteger)appProcessIdentifier +{ + if ((self = [super init])) { + _incidentIdentifier = incidentIdentifier; + _reporterKey = reporterKey; + _signal = signal; + _exceptionName = exceptionName; + _exceptionReason = exceptionReason; + _appStartTime = appStartTime; + _crashTime = crashTime; + _osVersion = osVersion; + _osBuild = osBuild; + _appVersion = appVersion; + _appBuild = appBuild; + _appProcessIdentifier = appProcessIdentifier; + } + return self; +} + +- (BOOL)isAppKill { + BOOL result = NO; + + if (self.signal && [[self.signal uppercaseString] isEqualToString:kBITCrashKillSignal]) + result = YES; + + return result; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashDetailsPrivate.h b/submodules/HockeySDK-iOS/Classes/BITCrashDetailsPrivate.h new file mode 100644 index 0000000000..48b3061344 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashDetailsPrivate.h @@ -0,0 +1,48 @@ +/* + * 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. + */ + +extern NSString *const kBITCrashKillSignal; + +@interface BITCrashDetails () { + +} + +- (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier + reporterKey:(NSString *)reporterKey + signal:(NSString *)signal + exceptionName:(NSString *)exceptionName + exceptionReason:(NSString *)exceptionReason + appStartTime:(NSDate *)appStartTime + crashTime:(NSDate *)crashTime + osVersion:(NSString *)osVersion + osBuild:(NSString *)osBuild + appVersion:(NSString *)appVersion + appBuild:(NSString *)appBuild + appProcessIdentifier:(NSUInteger)appProcessIdentifier; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashManager.h b/submodules/HockeySDK-iOS/Classes/BITCrashManager.h new file mode 100644 index 0000000000..69e7500f80 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashManager.h @@ -0,0 +1,441 @@ +/* + * Author: Andreas Linde + * Kent Sutherland + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde & Kent Sutherland. + * 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 + +#import "BITHockeyBaseManager.h" + +@class BITCrashDetails; +@class BITCrashMetaData; + + +/** + * Custom block that handles the alert that prompts the user whether they want to send crash reports + */ +typedef void(^BITCustomAlertViewHandler)(void); + + +/** + * Crash Manager status + */ +typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { + /** + * Crash reporting is disabled + */ + BITCrashManagerStatusDisabled = 0, + /** + * User is asked each time before sending + */ + BITCrashManagerStatusAlwaysAsk = 1, + /** + * Each crash report is send automatically + */ + BITCrashManagerStatusAutoSend = 2 +}; + + +/** + * Prototype of a callback function used to execute additional user code. Called upon completion of crash + * handling, after the crash report has been written to disk. + * + * @param context The API client's supplied context value. + * + * @see `BITCrashManagerCallbacks` + * @see `[BITCrashManager setCrashCallbacks:]` + */ +typedef void (*BITCrashManagerPostCrashSignalCallback)(void *context); + +/** + * This structure contains callbacks supported by `BITCrashManager` to allow the host application to perform + * additional tasks prior to program termination after a crash has occurred. + * + * @see `BITCrashManagerPostCrashSignalCallback` + * @see `[BITCrashManager setCrashCallbacks:]` + */ +typedef struct BITCrashManagerCallbacks { + /** An arbitrary user-supplied context value. This value may be NULL. */ + void *context; + + /** + * The callback used to report caught signal information. + */ + BITCrashManagerPostCrashSignalCallback handleSignal; +} BITCrashManagerCallbacks; + + +/** + * Crash Manager alert user input + */ +typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { + /** + * User chose not to send the crash report + */ + BITCrashManagerUserInputDontSend = 0, + /** + * User wants the crash report to be sent + */ + BITCrashManagerUserInputSend = 1, + /** + * User chose to always send crash reports + */ + BITCrashManagerUserInputAlwaysSend = 2 + +}; + + +@protocol BITCrashManagerDelegate; + +/** + The crash reporting module. + + This is the HockeySDK module for handling crash reports, including when distributed via the App Store. + As a foundation it is using the open source, reliable and async-safe crash reporting framework + [PLCrashReporter](https://code.google.com/p/plcrashreporter/). + + This module works as a wrapper around the underlying crash reporting framework and provides functionality to + detect new crashes, queues them if networking is not available, present a user interface to approve sending + the reports to the HockeyApp servers and more. + + It also provides options to add additional meta information to each crash report, like `userName`, `userEmail` + via `BITHockeyManagerDelegate` protocol, and additional textual log information via `BITCrashManagerDelegate` + protocol and a way to detect startup crashes so you can adjust your startup process to get these crash reports + too and delay your app initialization. + + Crashes are send the next time the app starts. If `crashManagerStatus` is set to `BITCrashManagerStatusAutoSend`, + crashes will be send without any user interaction, otherwise an alert will appear allowing the users to decide + whether they want to send the report or not. This module is not sending the reports right when the crash happens + deliberately, because if is not safe to implement such a mechanism while being async-safe (any Objective-C code + is _NOT_ async-safe!) and not causing more danger like a deadlock of the device, than helping. We found that users + do start the app again because most don't know what happened, and you will get by far most of the reports. + + Sending the reports on startup is done asynchronously (non-blocking). This is the only safe way to ensure + that the app won't be possibly killed by the iOS watchdog process, because startup could take too long + and the app could not react to any user input when network conditions are bad or connectivity might be + very slow. + + It is possible to check upon startup if the app crashed before using `didCrashInLastSession` and also how much + time passed between the app launch and the crash using `timeIntervalCrashInLastSessionOccurred`. This allows you + to add additional code to your app delaying the app start until the crash has been successfully send if the crash + occurred within a critical startup timeframe, e.g. after 10 seconds. The `BITCrashManagerDelegate` protocol provides + various delegates to inform the app about it's current status so you can continue the remaining app startup setup + after sending has been completed. The documentation contains a guide + [How to handle Crashes on startup](HowTo-Handle-Crashes-On-Startup) with an example on how to do that. + + More background information on this topic can be found in the following blog post by Landon Fuller, the + developer of [PLCrashReporter](https://www.plcrashreporter.org), about writing reliable and + safe crash reporting: [Reliable Crash Reporting](http://goo.gl/WvTBR) + + @warning If you start the app with the Xcode debugger attached, detecting crashes will _NOT_ be enabled! + */ + +@interface BITCrashManager : BITHockeyBaseManager + + +///----------------------------------------------------------------------------- +/// @name Configuration +///----------------------------------------------------------------------------- + +/** Set the default status of the Crash Manager + + Defines if the crash reporting feature should be disabled, ask the user before + sending each crash report or send crash reports automatically without + asking. + + The default value is `BITCrashManagerStatusAlwaysAsk`. The user can switch to + `BITCrashManagerStatusAutoSend` by choosing "Always" in the dialog (since + `showAlwaysButton` default is _YES_). + + The current value is always stored in User Defaults with the key + `BITCrashManagerStatus`. + + If you intend to implement a user setting to let them enable or disable + crash reporting, this delegate should be used to return that value. You also + have to make sure the new value is stored in the UserDefaults with the key + `BITCrashManagerStatus`. + + @see BITCrashManagerStatus + @see showAlwaysButton + */ +@property (nonatomic, assign) BITCrashManagerStatus crashManagerStatus; + + +/** + * Trap fatal signals via a Mach exception server. + * + * By default the SDK is using the safe and proven in-process BSD Signals for catching crashes. + * This option provides an option to enable catching fatal signals via a Mach exception server + * instead. + * + * We strongly advice _NOT_ to enable Mach exception handler in release versions of your apps! + * + * Default: _NO_ + * + * @warning The Mach exception handler executes in-process, and will interfere with debuggers when + * they attempt to suspend all active threads (which will include the Mach exception handler). + * Mach-based handling should _NOT_ be used when a debugger is attached. The SDK will not + * enabled catching exceptions if the app is started with the debugger running. If you attach + * the debugger during runtime, this may cause issues the Mach exception handler is enabled! + * @see isDebuggerAttached + */ +@property (nonatomic, assign, getter=isMachExceptionHandlerEnabled) BOOL enableMachExceptionHandler; + + +/** + * Enable on device symbolication for system symbols + * + * By default, the SDK does not symbolicate on the device, since this can + * take a few seconds at each crash. Also note that symbolication on the + * device might not be able to retrieve all symbols. + * + * Enable if you want to analyze crashes on unreleased OS versions. + * + * Default: _NO_ + */ +@property (nonatomic, assign, getter=isOnDeviceSymbolicationEnabled) BOOL enableOnDeviceSymbolication; + + +/** + * EXPERIMENTAL: Enable heuristics to detect the app not terminating cleanly + * + * This allows it to get a crash report if the app got killed while being in the foreground + * because of one of the following reasons: + * + * - The main thread was blocked for too long + * - The app took too long to start up + * - The app tried to allocate too much memory. If iOS did send a memory warning before killing the app because of this reason, `didReceiveMemoryWarningInLastSession` returns `YES`. + * - Permitted background duration if main thread is running in an endless loop + * - App failed to resume in time if main thread is running in an endless loop + * - If `enableMachExceptionHandler` is not activated, crashed due to stack overflow will also be reported + * + * The following kills can _NOT_ be detected: + * + * - Terminating the app takes too long + * - Permitted background duration too long for all other cases + * - App failed to resume in time for all other cases + * - possibly more cases + * + * Crash reports triggered by this mechanisms do _NOT_ contain any stack traces since the time of the kill + * cannot be intercepted and hence no stack trace of the time of the kill event can't be gathered. + * + * The heuristic is implemented as follows: + * If the app never gets a `UIApplicationDidEnterBackgroundNotification` or `UIApplicationWillTerminateNotification` + * notification, PLCrashReporter doesn't detect a crash itself, and the app starts up again, it is assumed that + * the app got either killed by iOS while being in foreground or a crash occurred that couldn't be detected. + * + * Default: _NO_ + * + * @warning This is a heuristic and it _MAY_ report false positives! It has been tested with iOS 6.1 and iOS 7. + * Depending on Apple changing notification events, new iOS version may cause more false positives! + * + * @see lastSessionCrashDetails + * @see didReceiveMemoryWarningInLastSession + * @see `BITCrashManagerDelegate considerAppNotTerminatedCleanlyReportForCrashManager:` + * @see [Apple Technical Note TN2151](https://developer.apple.com/library/ios/technotes/tn2151/_index.html) + * @see [Apple Technical Q&A QA1693](https://developer.apple.com/library/ios/qa/qa1693/_index.html) + */ +@property (nonatomic, assign, getter = isAppNotTerminatingCleanlyDetectionEnabled) BOOL enableAppNotTerminatingCleanlyDetection; + + +/** + * Set the callbacks that will be executed prior to program termination after a crash has occurred + * + * PLCrashReporter provides support for executing an application specified function in the context + * of the crash reporter's signal handler, after the crash report has been written to disk. + * + * Writing code intended for execution inside of a signal handler is exceptionally difficult, and is _NOT_ recommended! + * + * _Program Flow and Signal Handlers_ + * + * When the signal handler is called the normal flow of the program is interrupted, and your program is an unknown state. Locks may be held, the heap may be corrupt (or in the process of being updated), and your signal handler may invoke a function that was being executed at the time of the signal. This may result in deadlocks, data corruption, and program termination. + * + * _Async-Safe Functions_ + * + * A subset of functions are defined to be async-safe by the OS, and are safely callable from within a signal handler. If you do implement a custom post-crash handler, it must be async-safe. A table of POSIX-defined async-safe functions and additional information is available from the [CERT programming guide - SIG30-C](https://www.securecoding.cert.org/confluence/display/seccode/SIG30-C.+Call+only+asynchronous-safe+functions+within+signal+handlers). + * + * Most notably, the Objective-C runtime itself is not async-safe, and Objective-C may not be used within a signal handler. + * + * Documentation taken from PLCrashReporter: https://www.plcrashreporter.org/documentation/api/v1.2-rc2/async_safety.html + * + * @see BITCrashManagerPostCrashSignalCallback + * @see BITCrashManagerCallbacks + * + * @param callbacks A pointer to an initialized PLCrashReporterCallback structure, see https://www.plcrashreporter.org/documentation/api/v1.2-rc2/struct_p_l_crash_reporter_callbacks.html + */ +- (void)setCrashCallbacks: (BITCrashManagerCallbacks *) callbacks; + + +/** + Flag that determines if an "Always" option should be shown + + If enabled the crash reporting alert will also present an "Always" option, so + the user doesn't have to approve every single crash over and over again. + + If If `crashManagerStatus` is set to `BITCrashManagerStatusAutoSend`, this property + has no effect, since no alert will be presented. + + Default: _YES_ + + @see crashManagerStatus + */ +@property (nonatomic, assign, getter=shouldShowAlwaysButton) BOOL showAlwaysButton; + + +///----------------------------------------------------------------------------- +/// @name Crash Meta Information +///----------------------------------------------------------------------------- + +/** + Indicates if the app crash in the previous session + + Use this on startup, to check if the app starts the first time after it crashed + previously. You can use this also to disable specific events, like asking + the user to rate your app. + + @warning This property only has a correct value, once `[BITHockeyManager startManager]` was + invoked! + + @see lastSessionCrashDetails + */ +@property (nonatomic, readonly) BOOL didCrashInLastSession; + +/** + Provides an interface to pass user input from a custom alert to a crash report + + @param userInput Defines the users action wether to send, always send, or not to send the crash report. + @param userProvidedMetaData The content of this optional BITCrashMetaData instance will be attached to the crash report and allows to ask the user for e.g. additional comments or info. + + @return Returns YES if the input is a valid option and successfully triggered further processing of the crash report + + @see BITCrashManagerUserInput + @see BITCrashMetaData + */ +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData; + +/** + Lets you set a custom block which handles showing a custom UI and asking the user + whether they want to send the crash report. + + This replaces the default alert the SDK would show! + + You can use this to present any kind of user interface which asks the user for additional information, + e.g. what they did in the app before the app crashed. + + In addition to this you should always ask your users if they agree to send crash reports, send them + always or not and return the result when calling `handleUserInput:withUserProvidedCrashDescription`. + + @param alertViewHandler A block that is responsible for loading, presenting and and dismissing your custom user interface which prompts the user if they want to send crash reports. The block is also responsible for triggering further processing of the crash reports. + + @warning This is not available when compiled for Watch OS! + + @warning Block needs to call the `[BITCrashManager handleUserInput:withUserProvidedMetaData:]` method! + + @warning This needs to be set before calling `[BITHockeyManager startManager]`! + */ +- (void)setAlertViewHandler:(BITCustomAlertViewHandler)alertViewHandler; + +/** + * Provides details about the crash that occurred in the last app session + */ +@property (nonatomic, readonly) BITCrashDetails *lastSessionCrashDetails; + + +/** + Indicates if the app did receive a low memory warning in the last session + + It may happen that low memory warning where send but couldn't be logged, since iOS + killed the app before updating the flag in the filesystem did complete. + + This property may be true in case of low memory kills, but it doesn't have to be! Apps + can also be killed without the app ever receiving a low memory warning. + + Also the app could have received a low memory warning, but the reason for being killed was + actually different. + + @warning This property only has a correct value, once `[BITHockeyManager startManager]` was + invoked! + + @see enableAppNotTerminatingCleanlyDetection + @see lastSessionCrashDetails + */ +@property (nonatomic, readonly) BOOL didReceiveMemoryWarningInLastSession; + + +/** + Provides the time between startup and crash in seconds + + Use this in together with `didCrashInLastSession` to detect if the app crashed very + early after startup. This can be used to delay app initialization until the crash + report has been sent to the server or if you want to do any other actions like + cleaning up some cache data etc. + + Note that sending a crash reports starts as early as 1.5 seconds after the application + did finish launching! + + The `BITCrashManagerDelegate` protocol provides some delegates to inform if sending + a crash report was finished successfully, ended in error or was cancelled by the user. + + *Default*: _-1_ + @see didCrashInLastSession + @see BITCrashManagerDelegate + */ +@property (nonatomic, readonly) NSTimeInterval timeIntervalCrashInLastSessionOccurred; + + +///----------------------------------------------------------------------------- +/// @name Helper +///----------------------------------------------------------------------------- + +/** + * Detect if a debugger is attached to the app process + * + * This is only invoked once on app startup and can not detect if the debugger is being + * attached during runtime! + * + * @return BOOL if the debugger is attached on app startup + */ +- (BOOL)isDebuggerAttached; + + +/** + * Lets the app crash for easy testing of the SDK + * + * The best way to use this is to trigger the crash with a button action. + * + * Make sure not to let the app crash in `applicationDidFinishLaunching` or any other + * startup method! Since otherwise the app would crash before the SDK could process it. + * + * Note that our SDK provides support for handling crashes that happen early on startup. + * Check the documentation for more information on how to use this. + * + * If the SDK detects an App Store environment, it will _NOT_ cause the app to crash! + */ +- (void)generateTestCrash; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashManager.m b/submodules/HockeySDK-iOS/Classes/BITCrashManager.m new file mode 100644 index 0000000000..6b60dcc00a --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashManager.m @@ -0,0 +1,1713 @@ +/* + * Author: Andreas Linde + * Kent Sutherland + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde & Kent Sutherland. + * 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 "HockeySDKFeatureConfig.h" + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER + +#import +#import + +#import "HockeySDKPrivate.h" +#import "BITHockeyHelper.h" +#import "BITHockeyHelper+Application.h" +#import "BITHockeyAppClient.h" + +#import "BITCrashManager.h" +#import "BITCrashManagerPrivate.h" +#import "BITCrashAttachment.h" +#import "BITHockeyBaseManagerPrivate.h" +#import "BITCrashReportTextFormatter.h" +#import "BITCrashDetailsPrivate.h" +#import "BITCrashCXXExceptionHandler.h" + +#if HOCKEYSDK_FEATURE_METRICS +#import "BITMetricsManagerPrivate.h" +#import "BITChannel.h" +#import "BITPersistencePrivate.h" +#endif + +#include + +// stores the set of crashreports that have been approved but aren't sent yet +#define kBITCrashApprovedReports @"HockeySDKCrashApprovedReports" + +// keys for meta information associated to each crash +#define kBITCrashMetaUserName @"BITCrashMetaUserName" +#define kBITCrashMetaUserEmail @"BITCrashMetaUserEmail" +#define kBITCrashMetaUserID @"BITCrashMetaUserID" +#define kBITCrashMetaApplicationLog @"BITCrashMetaApplicationLog" +#define kBITCrashMetaAttachment @"BITCrashMetaAttachment" + +// internal keys +static NSString *const KBITAttachmentDictIndex = @"index"; +static NSString *const KBITAttachmentDictAttachment = @"attachment"; + +static NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; + +static NSString *const kBITAppWentIntoBackgroundSafely = @"BITAppWentIntoBackgroundSafely"; +static NSString *const kBITAppDidReceiveLowMemoryNotification = @"BITAppDidReceiveLowMemoryNotification"; +static NSString *const kBITAppMarketingVersion = @"BITAppMarketingVersion"; +static NSString *const kBITAppVersion = @"BITAppVersion"; +static NSString *const kBITAppOSVersion = @"BITAppOSVersion"; +static NSString *const kBITAppOSBuild = @"BITAppOSBuild"; +static NSString *const kBITAppUUIDs = @"BITAppUUIDs"; + +static NSString *const kBITFakeCrashUUID = @"BITFakeCrashUUID"; +static NSString *const kBITFakeCrashAppMarketingVersion = @"BITFakeCrashAppMarketingVersion"; +static NSString *const kBITFakeCrashAppVersion = @"BITFakeCrashAppVersion"; +static NSString *const kBITFakeCrashAppBundleIdentifier = @"BITFakeCrashAppBundleIdentifier"; +static NSString *const kBITFakeCrashOSVersion = @"BITFakeCrashOSVersion"; +static NSString *const kBITFakeCrashDeviceModel = @"BITFakeCrashDeviceModel"; +static NSString *const kBITFakeCrashAppBinaryUUID = @"BITFakeCrashAppBinaryUUID"; +static NSString *const kBITFakeCrashReport = @"BITFakeCrashAppString"; + +// We need BIT_UNUSED macro to make sure there aren't any warnings when building +// HockeySDK Distribution scheme. Since several configurations are build in this scheme +// and different features can be turned on and off we can't just use __unused attribute. +#if HOCKEYSDK_FEATURE_METRICS +static char const *BITSaveEventsFilePath; +#define BIT_UNUSED +#else +#define BIT_UNUSED __unused +#endif + +static BITCrashManagerCallbacks bitCrashCallbacks = { + .context = NULL, + .handleSignal = NULL +}; + +#if HOCKEYSDK_FEATURE_METRICS +static void bit_save_events_callback(siginfo_t __unused *info, ucontext_t __unused *uap, void __unused *context) { + + // Do not flush metrics queue if queue is empty (metrics module disabled) to not freeze the app + if (!BITTelemetryEventBuffer) { + return; + } + + // Try to get a file descriptor with our pre-filled path + int fd = open(BITSaveEventsFilePath, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { + return; + } + + size_t len = strlen(BITTelemetryEventBuffer); + if (len > 0) { + // Simply write the whole string to disk + write(fd, BITTelemetryEventBuffer, len); + } + close(fd); +} +#endif + +// Proxy implementation for PLCrashReporter to keep our interface stable while this can change +static void plcr_post_crash_callback (BIT_UNUSED siginfo_t *info, BIT_UNUSED ucontext_t *uap, void *context) { +#if HOCKEYSDK_FEATURE_METRICS + bit_save_events_callback(info, uap, context); +#endif + if (bitCrashCallbacks.handleSignal != NULL) { + bitCrashCallbacks.handleSignal(context); + } +} + +static PLCrashReporterCallbacks plCrashCallbacks = { + .version = 0, + .context = NULL, + .handleSignal = plcr_post_crash_callback +}; + +// Temporary class until PLCR catches up +// We trick PLCR with an Objective-C exception. +// +// This code provides us access to the C++ exception message, including a correct stack trace. +// +@interface BITCrashCXXExceptionWrapperException : NSException + +- (instancetype)initWithCXXExceptionInfo:(const BITCrashUncaughtCXXExceptionInfo *)info; + +@property (nonatomic, readonly) const BITCrashUncaughtCXXExceptionInfo *info; + +@end + +@implementation BITCrashCXXExceptionWrapperException + +- (instancetype)initWithCXXExceptionInfo:(const BITCrashUncaughtCXXExceptionInfo *)info { + extern char* __cxa_demangle(const char* mangled_name, char* output_buffer, size_t* length, int* status); + char *demangled_name = &__cxa_demangle ? __cxa_demangle(info->exception_type_name ?: "", NULL, NULL, NULL) : NULL; + + if ((self = [super + initWithName:(NSString *)[NSString stringWithUTF8String:demangled_name ?: info->exception_type_name ?: ""] + reason:[NSString stringWithUTF8String:info->exception_message ?: ""] + userInfo:nil])) { + _info = info; + } + return self; +} + +- (NSArray *)callStackReturnAddresses { + NSMutableArray *cxxFrames = [NSMutableArray arrayWithCapacity:self.info->exception_frames_count]; + + for (uint32_t i = 0; i < self.info->exception_frames_count; ++i) { + [cxxFrames addObject:[NSNumber numberWithUnsignedLongLong:self.info->exception_frames[i]]]; + } + return cxxFrames; +} + +@end + + +// C++ Exception Handler +__attribute__((noreturn)) static void uncaught_cxx_exception_handler(const BITCrashUncaughtCXXExceptionInfo *info) { + // This relies on a LOT of sneaky internal knowledge of how PLCR works and should not be considered a long-term solution. + NSGetUncaughtExceptionHandler()([[BITCrashCXXExceptionWrapperException alloc] initWithCXXExceptionInfo:info]); + abort(); +} + +@interface BITCrashManager () + +@property (nonatomic, strong) NSMutableDictionary *approvedCrashReports; +@property (nonatomic, strong) NSMutableArray *crashFiles; +@property (nonatomic, copy) NSString *settingsFile; +@property (nonatomic, copy) NSString *analyzerInProgressFile; +@property (nonatomic) BOOL crashIdenticalCurrentVersion; +@property (nonatomic) BOOL sendingInProgress; +@property (nonatomic) BOOL isSetup; +@property (nonatomic) BOOL didLogLowMemoryWarning; +@property (nonatomic, weak) id appDidBecomeActiveObserver; +@property (nonatomic, weak) id appWillTerminateObserver; +@property (nonatomic, weak) id appDidEnterBackgroundObserver; +@property (nonatomic, weak) id appWillEnterForegroundObserver; +@property (nonatomic, weak) id appDidReceiveLowMemoryWarningObserver; +@property (nonatomic, weak) id networkDidBecomeReachableObserver; + +// Redeclare BITCrashManager properties with readwrite attribute +@property (nonatomic, readwrite) NSTimeInterval timeIntervalCrashInLastSessionOccurred; +@property (nonatomic, readwrite) BITCrashDetails *lastSessionCrashDetails; +@property (nonatomic, readwrite) BOOL didCrashInLastSession; +@property (nonatomic, readwrite) BOOL didReceiveMemoryWarningInLastSession; + +@end + +@implementation BITCrashManager + +- (instancetype)initWithAppIdentifier:(NSString *)appIdentifier appEnvironment:(BITEnvironment)environment hockeyAppClient:(BITHockeyAppClient *)hockeyAppClient { + if ((self = [super initWithAppIdentifier:appIdentifier appEnvironment:environment])) { + _delegate = nil; + _isSetup = NO; + + _hockeyAppClient = hockeyAppClient; + + _showAlwaysButton = YES; + _alertViewHandler = nil; + + _plCrashReporter = nil; + _exceptionHandler = nil; + + _crashIdenticalCurrentVersion = YES; + + _didCrashInLastSession = NO; + _timeIntervalCrashInLastSessionOccurred = -1; + _didLogLowMemoryWarning = NO; + + _approvedCrashReports = [[NSMutableDictionary alloc] init]; + + _fileManager = [[NSFileManager alloc] init]; + _crashFiles = [[NSMutableArray alloc] init]; + + _crashManagerStatus = BITCrashManagerStatusAlwaysAsk; + + if ([[NSUserDefaults standardUserDefaults] stringForKey:kBITCrashManagerStatus]) { + _crashManagerStatus = (BITCrashManagerStatus)[[NSUserDefaults standardUserDefaults] integerForKey:kBITCrashManagerStatus]; + } else { + // migrate previous setting if available + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"BITCrashAutomaticallySendReports"]) { + _crashManagerStatus = BITCrashManagerStatusAutoSend; + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"BITCrashAutomaticallySendReports"]; + } + [[NSUserDefaults standardUserDefaults] setInteger:_crashManagerStatus forKey:kBITCrashManagerStatus]; + } + + _crashesDir = bit_settingsDir(); + _settingsFile = [_crashesDir stringByAppendingPathComponent:BITHOCKEY_CRASH_SETTINGS]; + _analyzerInProgressFile = [_crashesDir stringByAppendingPathComponent:BITHOCKEY_CRASH_ANALYZER]; + + + if (!BITHockeyBundle() && !bit_isRunningInAppExtension()) { + BITHockeyLogWarning(@"[HockeySDK] WARNING: %@ is missing, will send reports automatically!", BITHOCKEYSDK_BUNDLE); + } + } + return self; +} + + +- (void) dealloc { + [self unregisterObservers]; +} + + +- (void)setCrashManagerStatus:(BITCrashManagerStatus)crashManagerStatus { + _crashManagerStatus = crashManagerStatus; + + [[NSUserDefaults standardUserDefaults] setInteger:crashManagerStatus forKey:kBITCrashManagerStatus]; +} + +- (void)setServerURL:(NSString *)serverURL { + if ([serverURL isEqualToString:super.serverURL]) { return; } + + super.serverURL = serverURL; + self.hockeyAppClient = [[BITHockeyAppClient alloc] initWithBaseURL:[NSURL URLWithString:serverURL]]; +} + +#pragma mark - Private + +/** + * Save all settings + * + * This saves the list of approved crash reports + */ +- (void)saveSettings { + NSError *error = nil; + + NSMutableDictionary *rootObj = [NSMutableDictionary dictionaryWithCapacity:2]; + if (self.approvedCrashReports && [self.approvedCrashReports count] > 0) { + [rootObj setObject:self.approvedCrashReports forKey:kBITCrashApprovedReports]; + } + + NSData *plist = [NSPropertyListSerialization dataWithPropertyList:(id)rootObj format:NSPropertyListBinaryFormat_v1_0 options:0 error:&error]; + + if (plist) { + [plist writeToFile:self.settingsFile atomically:YES]; + } else { + BITHockeyLogError(@"ERROR: Writing settings. %@", [error description]); + } +} + +/** + * Load all settings + * + * This contains the list of approved crash reports + */ +- (void)loadSettings { + NSError *error = nil; + NSPropertyListFormat format; + + if (![self.fileManager fileExistsAtPath:self.settingsFile]) + return; + + NSData *plist = [NSData dataWithContentsOfFile:self.settingsFile]; + if (plist) { + NSDictionary *rootObj = (NSDictionary *)[NSPropertyListSerialization + propertyListWithData:plist + options:NSPropertyListMutableContainersAndLeaves + format:&format + error:&error]; + + if ([rootObj objectForKey:kBITCrashApprovedReports]) + [self.approvedCrashReports setDictionary:(NSDictionary *)[rootObj objectForKey:kBITCrashApprovedReports]]; + } else { + BITHockeyLogError(@"ERROR: Reading crash manager settings."); + } +} + + +/** + * Remove a cached crash report + * + * @param filename The base filename of the crash report + */ +- (void)cleanCrashReportWithFilename:(NSString *)filename { + if (!filename) return; + + NSError *error = NULL; + + [self.fileManager removeItemAtPath:filename error:&error]; + [self.fileManager removeItemAtPath:[filename stringByAppendingString:@".data"] error:&error]; + [self.fileManager removeItemAtPath:[filename stringByAppendingString:@".meta"] error:&error]; + [self.fileManager removeItemAtPath:[filename stringByAppendingString:@".desc"] error:&error]; + + NSString *cacheFilename = [filename lastPathComponent]; + [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; + [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; + [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; + + [self.crashFiles removeObject:filename]; + [self.approvedCrashReports removeObjectForKey:filename]; + + [self saveSettings]; +} + +/** + * Remove all crash reports and stored meta data for each from the file system and keychain + * + * This is currently only used as a helper method for tests + */ +- (void)cleanCrashReports { + for (NSUInteger i=0; i < [self.crashFiles count]; i++) { + [self cleanCrashReportWithFilename:[self.crashFiles objectAtIndex:i]]; + } +} + +- (BOOL)persistAttachment:(BITHockeyAttachment *)attachment withFilename:(NSString *)filename { + NSString *attachmentFilename = [filename stringByAppendingString:@".data"]; + NSMutableData *data = [[NSMutableData alloc] init]; + NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; + + [archiver encodeObject:attachment forKey:kBITCrashMetaAttachment]; + + [archiver finishEncoding]; + + return [data writeToFile:attachmentFilename atomically:YES]; +} + +- (void)persistUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData { + if (!userProvidedMetaData) return; + + if (userProvidedMetaData.userProvidedDescription && [userProvidedMetaData.userProvidedDescription length] > 0) { + NSError *error; + [userProvidedMetaData.userProvidedDescription writeToFile:[NSString stringWithFormat:@"%@.desc", [self.crashesDir stringByAppendingPathComponent: self.lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; + } + + if (userProvidedMetaData.userName && [userProvidedMetaData.userName length] > 0) { + [self addStringValueToKeychain:userProvidedMetaData.userName forKey:[NSString stringWithFormat:@"%@.%@", self.lastCrashFilename, kBITCrashMetaUserName]]; + + } + + if (userProvidedMetaData.userEmail && [userProvidedMetaData.userEmail length] > 0) { + [self addStringValueToKeychain:userProvidedMetaData.userEmail forKey:[NSString stringWithFormat:@"%@.%@", self.lastCrashFilename, kBITCrashMetaUserEmail]]; + } + + if (userProvidedMetaData.userID && [userProvidedMetaData.userID length] > 0) { + [self addStringValueToKeychain:userProvidedMetaData.userID forKey:[NSString stringWithFormat:@"%@.%@", self.lastCrashFilename, kBITCrashMetaUserID]]; + + } +} + +/** + * Read the attachment data from the stored file + * + * @param filename The crash report file path + * + * @return an BITHockeyAttachment instance or nil + */ +- (BITHockeyAttachment *)attachmentForCrashReport:(NSString *)filename { + NSString *attachmentFilename = [filename stringByAppendingString:@".data"]; + + if (![self.fileManager fileExistsAtPath:attachmentFilename]) + return nil; + + + NSData *codedData = [[NSData alloc] initWithContentsOfFile:attachmentFilename]; + if (!codedData) + return nil; + + NSKeyedUnarchiver *unarchiver = nil; + + @try { + unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:codedData]; + } + @catch (NSException __unused *exception) { + return nil; + } + + if ([unarchiver containsValueForKey:kBITCrashMetaAttachment]) { + BITHockeyAttachment *attachment = [unarchiver decodeObjectForKey:kBITCrashMetaAttachment]; + return attachment; + } + + return nil; +} + +/** + * Extract all app specific UUIDs from the crash reports + * + * This allows us to send the UUIDs in the XML construct to the server, so the server does not need to parse the crash report for this data. + * The app specific UUIDs help to identify which dSYMs are needed to symbolicate this crash report. + * + * @param report The crash report from PLCrashReporter + * + * @return XML structure with the app specific UUIDs + */ +- (NSString *) extractAppUUIDs:(BITPLCrashReport *)report { + NSMutableString *uuidString = [NSMutableString string]; + NSArray *uuidArray = [BITCrashReportTextFormatter arrayOfAppUUIDsForCrashReport:report]; + + for (NSDictionary *element in uuidArray) { + if ([element objectForKey:kBITBinaryImageKeyType] && [element objectForKey:kBITBinaryImageKeyArch] && [element objectForKey:kBITBinaryImageKeyUUID]) { + [uuidString appendFormat:@"%@", + [element objectForKey:kBITBinaryImageKeyType], + [element objectForKey:kBITBinaryImageKeyArch], + [element objectForKey:kBITBinaryImageKeyUUID] + ]; + } + } + + return uuidString; +} + +- (void) registerObservers { + __weak typeof(self) weakSelf = self; + + if(nil == self.appDidBecomeActiveObserver) { + NSNotificationName name = UIApplicationDidBecomeActiveNotification; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + if (bit_isRunningInAppExtension() && &NSExtensionHostDidBecomeActiveNotification != NULL) { + name = NSExtensionHostDidBecomeActiveNotification; + } +#pragma clang diagnostic pop + self.appDidBecomeActiveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:name + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf triggerDelayedProcessing]; + }]; + } + + if(nil == self.networkDidBecomeReachableObserver) { + self.networkDidBecomeReachableObserver = [[NSNotificationCenter defaultCenter] addObserverForName:BITHockeyNetworkDidBecomeReachableNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf triggerDelayedProcessing]; + }]; + } + + if (nil == self.appWillTerminateObserver) { + self.appWillTerminateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf leavingAppSafely]; + }]; + } + + if (nil == self.appDidEnterBackgroundObserver) { + NSNotificationName name = UIApplicationDidEnterBackgroundNotification; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + if (bit_isRunningInAppExtension() && &NSExtensionHostDidEnterBackgroundNotification != NULL) { + name = NSExtensionHostDidEnterBackgroundNotification; + } +#pragma clang diagnostic pop + self.appDidEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:name + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf leavingAppSafely]; + }]; + } + + if (nil == self.appWillEnterForegroundObserver) { + NSNotificationName name = UIApplicationWillEnterForegroundNotification; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + if (bit_isRunningInAppExtension() && &NSExtensionHostWillEnterForegroundNotification != NULL) { + name = NSExtensionHostWillEnterForegroundNotification; + } +#pragma clang diagnostic pop + self.appWillEnterForegroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:name + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf appEnteredForeground]; + }]; + } + + if (nil == self.appDidReceiveLowMemoryWarningObserver) { + if (bit_isRunningInAppExtension()) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0, DISPATCH_MEMORYPRESSURE_WARN|DISPATCH_MEMORYPRESSURE_CRITICAL, dispatch_get_main_queue()); + dispatch_source_set_event_handler(source, ^{ + if (!self.didLogLowMemoryWarning) { + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITAppDidReceiveLowMemoryNotification]; + self.didLogLowMemoryWarning = YES; + } + }); + dispatch_resume(source); + }); + } else { + self.appDidReceiveLowMemoryWarningObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + // we only need to log this once + if (!self.didLogLowMemoryWarning) { + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITAppDidReceiveLowMemoryNotification]; + self.didLogLowMemoryWarning = YES; + + } + }]; + } + } +} + +- (void) unregisterObservers { + [self unregisterObserver:self.appDidBecomeActiveObserver]; + [self unregisterObserver:self.appWillTerminateObserver]; + [self unregisterObserver:self.appDidEnterBackgroundObserver]; + [self unregisterObserver:self.appWillEnterForegroundObserver]; + [self unregisterObserver:self.appDidReceiveLowMemoryWarningObserver]; + + [self unregisterObserver:self.networkDidBecomeReachableObserver]; +} + +- (void) unregisterObserver:(id)observer { + if (observer) { + [[NSNotificationCenter defaultCenter] removeObserver:observer]; + observer = nil; + } +} + +- (void)leavingAppSafely { + if (self.isAppNotTerminatingCleanlyDetectionEnabled) { + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITAppWentIntoBackgroundSafely]; + } +} + +- (void)appEnteredForeground { + // we disable kill detection while the debugger is running, since we'd get only false positives if the app is terminated by the user using the debugger + if (self.isDebuggerAttached) { + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITAppWentIntoBackgroundSafely]; + } else if (self.isAppNotTerminatingCleanlyDetectionEnabled) { + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kBITAppWentIntoBackgroundSafely]; + + static dispatch_once_t predAppData; + + dispatch_once(&predAppData, ^{ + id marketingVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + if (marketingVersion && [marketingVersion isKindOfClass:[NSString class]]) + [[NSUserDefaults standardUserDefaults] setObject:marketingVersion forKey:kBITAppMarketingVersion]; + + id bundleVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; + if (bundleVersion && [bundleVersion isKindOfClass:[NSString class]]) + [[NSUserDefaults standardUserDefaults] setObject:bundleVersion forKey:kBITAppVersion]; + + [[NSUserDefaults standardUserDefaults] setObject:[[UIDevice currentDevice] systemVersion] forKey:kBITAppOSVersion]; + [[NSUserDefaults standardUserDefaults] setObject:[self osBuild] forKey:kBITAppOSBuild]; + + NSString *uuidString =[NSString stringWithFormat:@"%@", + [self deviceArchitecture], + [self executableUUID] + ]; + + [[NSUserDefaults standardUserDefaults] setObject:uuidString forKey:kBITAppUUIDs]; + }); + } +} + +- (NSString *)deviceArchitecture { + NSString *archName = @"???"; + + size_t size; + cpu_type_t type; + cpu_subtype_t subtype; + size = sizeof(type); + if (sysctlbyname("hw.cputype", &type, &size, NULL, 0)) + return archName; + + size = sizeof(subtype); + if (sysctlbyname("hw.cpusubtype", &subtype, &size, NULL, 0)) + return archName; + + archName = [BITCrashReportTextFormatter bit_archNameFromCPUType:type subType:subtype] ?: @"???"; + + return archName; +} + +- (NSString *)osBuild { + size_t size; + sysctlbyname("kern.osversion", NULL, &size, NULL, 0); + char *answer = (char*)malloc(size); + if (answer == NULL) + return nil; + sysctlbyname("kern.osversion", answer, &size, NULL, 0); + NSString *osBuild = [NSString stringWithCString:answer encoding: NSUTF8StringEncoding]; + free(answer); + return osBuild; +} + +/** + * Get the userID from the delegate which should be stored with the crash report + * + * @return The userID value + */ +- (NSString *)userIDForCrashReport { + NSString *userID; +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + // if we have an identification from BITAuthenticator, use this as a default. + if (( + self.installationIdentificationType == BITAuthenticatorIdentificationTypeAnonymous || + self.installationIdentificationType == BITAuthenticatorIdentificationTypeDevice + ) && + self.installationIdentification) { + userID = self.installationIdentification; + } +#endif + + // first check the global keychain storage + NSString *userIdFromKeychain = [self stringValueFromKeychainForKey:kBITHockeyMetaUserID]; + if (userIdFromKeychain) { + userID = userIdFromKeychain; + } + id strongDelegate = [BITHockeyManager sharedHockeyManager].delegate; + if ([strongDelegate respondsToSelector:@selector(userIDForHockeyManager:componentManager:)]) { + userID = [strongDelegate userIDForHockeyManager:[BITHockeyManager sharedHockeyManager] componentManager:self]; + } + return userID ?: @""; +} + +/** + * Get the userName from the delegate which should be stored with the crash report + * + * @return The userName value + */ +- (NSString *)userNameForCrashReport { + // first check the global keychain storage + NSString *username = [self stringValueFromKeychainForKey:kBITHockeyMetaUserName] ?: @""; + id strongDelegate = [BITHockeyManager sharedHockeyManager].delegate; + if ([strongDelegate respondsToSelector:@selector(userNameForHockeyManager:componentManager:)]) { + username = [strongDelegate userNameForHockeyManager:[BITHockeyManager sharedHockeyManager] componentManager:self] ?: @""; + } + return username; +} + +/** + * Get the userEmail from the delegate which should be stored with the crash report + * + * @return The userEmail value + */ +- (NSString *)userEmailForCrashReport { + // first check the global keychain storage + NSString *useremail = [self stringValueFromKeychainForKey:kBITHockeyMetaUserEmail] ?: @""; + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + // if we have an identification from BITAuthenticator, use this as a default. + if (( + self.installationIdentificationType == BITAuthenticatorIdentificationTypeHockeyAppEmail || + self.installationIdentificationType == BITAuthenticatorIdentificationTypeHockeyAppUser || + self.installationIdentificationType == BITAuthenticatorIdentificationTypeWebAuth + ) && + self.installationIdentification) { + useremail = self.installationIdentification; + } +#endif + id strongDelegate = [BITHockeyManager sharedHockeyManager].delegate; + if ([strongDelegate respondsToSelector:@selector(userEmailForHockeyManager:componentManager:)]) { + useremail = [strongDelegate userEmailForHockeyManager:[BITHockeyManager sharedHockeyManager] componentManager:self] ?: @""; + } + return useremail; +} + +#pragma mark - CrashCallbacks + +/** + * Set the callback for PLCrashReporter + * + * @param callbacks BITCrashManagerCallbacks instance + */ +- (void)setCrashCallbacks:(BITCrashManagerCallbacks *)callbacks { + if (!callbacks) return; + if (self.isSetup) { + BITHockeyLogWarning(@"WARNING: CrashCallbacks need to be configured before calling startManager!"); + } + + // set our proxy callback struct + bitCrashCallbacks.context = callbacks->context; + bitCrashCallbacks.handleSignal = callbacks->handleSignal; + + // set the PLCrashReporterCallbacks struct + plCrashCallbacks.context = callbacks->context; +} + +#if HOCKEYSDK_FEATURE_METRICS +- (void)configDefaultCrashCallback { + BITMetricsManager *metricsManager = [BITHockeyManager sharedHockeyManager].metricsManager; + BITPersistence *persistence = metricsManager.persistence; + BITSaveEventsFilePath = strdup([persistence fileURLForType:BITPersistenceTypeTelemetry].UTF8String); +} +#endif + +#pragma mark - Public + +- (void)setAlertViewHandler:(BITCustomAlertViewHandler)alertViewHandler{ + _alertViewHandler = alertViewHandler; +} + + +- (BOOL)isDebuggerAttached { + return bit_isDebuggerAttached(); +} + + +- (void)generateTestCrash { + if (self.appEnvironment != BITEnvironmentAppStore) { + + if ([self isDebuggerAttached]) { + BITHockeyLogWarning(@"[HockeySDK] WARNING: The debugger is attached. The following crash cannot be detected by the SDK!"); + } + + __builtin_trap(); + } +} + +/** + * Write a meta file for a new crash report + * + * @param filename the crash reports temp filename + */ +- (void)storeMetaDataForCrashReportFilename:(NSString *)filename { + BITHockeyLogVerbose(@"VERBOSE: Storing meta data for crash report with filename %@", filename); + NSError *error = NULL; + NSMutableDictionary *metaDict = [NSMutableDictionary dictionaryWithCapacity:4]; + NSString *applicationLog = @""; + + [self addStringValueToKeychain:[self userNameForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", filename, kBITCrashMetaUserName]]; + [self addStringValueToKeychain:[self userEmailForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", filename, kBITCrashMetaUserEmail]]; + [self addStringValueToKeychain:[self userIDForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", filename, kBITCrashMetaUserID]]; + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(applicationLogForCrashManager:)]) { + applicationLog = [strongDelegate applicationLogForCrashManager:self] ?: @""; + } + [metaDict setObject:applicationLog forKey:kBITCrashMetaApplicationLog]; + + if ([strongDelegate respondsToSelector:@selector(attachmentForCrashManager:)]) { + BITHockeyLogVerbose(@"VERBOSE: Processing attachment for crash report with filename %@", filename); + BITHockeyAttachment *attachment = [strongDelegate attachmentForCrashManager:self]; + + if (attachment && attachment.hockeyAttachmentData) { + BOOL success = [self persistAttachment:attachment withFilename:[self.crashesDir stringByAppendingPathComponent: filename]]; + if (!success) { + BITHockeyLogError(@"ERROR: Persisting the crash attachment failed"); + } else { + BITHockeyLogVerbose(@"VERBOSE: Crash attachment successfully persisted."); + } + } else { + BITHockeyLogDebug(@"INFO: Crash attachment was nil"); + } + } + + NSData *plist = [NSPropertyListSerialization dataWithPropertyList:(id)metaDict + format:NSPropertyListBinaryFormat_v1_0 + options:0 + error:&error]; + if (plist) { + BOOL success = [plist writeToFile:[self.crashesDir stringByAppendingPathComponent:(NSString *)[filename stringByAppendingPathExtension:@"meta"]] atomically:YES]; + if (!success) { + BITHockeyLogError(@"ERROR: Writing crash meta data failed."); + } + } else { + BITHockeyLogError(@"ERROR: Writing crash meta data failed. %@", error); + } + BITHockeyLogVerbose(@"VERBOSE: Storing crash meta data finished."); +} + +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData { + id strongDelegate = self.delegate; + switch (userInput) { + case BITCrashManagerUserInputDontSend: + if ([strongDelegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { + [strongDelegate crashManagerWillCancelSendingCrashReport:self]; + } + + if (self.lastCrashFilename) + [self cleanCrashReportWithFilename:[self.crashesDir stringByAppendingPathComponent: self.lastCrashFilename]]; + + return YES; + + case BITCrashManagerUserInputSend: + if (userProvidedMetaData) + [self persistUserProvidedMetaData:userProvidedMetaData]; + + [self approveLatestCrashReport]; + [self sendNextCrashReport]; + return YES; + + case BITCrashManagerUserInputAlwaysSend: + self.crashManagerStatus = BITCrashManagerStatusAutoSend; + [[NSUserDefaults standardUserDefaults] setInteger:self.crashManagerStatus forKey:kBITCrashManagerStatus]; + + if ([strongDelegate respondsToSelector:@selector(crashManagerWillSendCrashReportsAlways:)]) { + [strongDelegate crashManagerWillSendCrashReportsAlways:self]; + } + + if (userProvidedMetaData) + [self persistUserProvidedMetaData:userProvidedMetaData]; + + [self approveLatestCrashReport]; + [self sendNextCrashReport]; + return YES; + + default: + return NO; + } + +} + +#pragma mark - PLCrashReporter + +/** + * Process new crash reports provided by PLCrashReporter + * + * Parse the new crash report and gather additional meta data from the app which will be stored along the crash report + */ +- (void) handleCrashReport { + BITHockeyLogVerbose(@"VERBOSE: Handling crash report"); + NSError *error = NULL; + + if (!self.plCrashReporter) return; + + // check if the next call ran successfully the last time + if (![self.fileManager fileExistsAtPath:self.analyzerInProgressFile]) { + // mark the start of the routine + [self.fileManager createFileAtPath:self.analyzerInProgressFile contents:nil attributes:nil]; + BITHockeyLogVerbose(@"VERBOSE: AnalyzerInProgress file created"); + + [self saveSettings]; + + // Try loading the crash report + NSData *crashData = [[NSData alloc] initWithData:[self.plCrashReporter loadPendingCrashReportDataAndReturnError: &error]]; + + NSString *cacheFilename = [NSString stringWithFormat: @"%.0f", [NSDate timeIntervalSinceReferenceDate]]; + self.lastCrashFilename = cacheFilename; + + if (crashData == nil) { + BITHockeyLogError(@"ERROR: Could not load crash report: %@", error); + } else { + // get the startup timestamp from the crash report, and the file timestamp to calculate the timeinterval when the crash happened after startup + BITPLCrashReport *report = [[BITPLCrashReport alloc] initWithData:crashData error:&error]; + + if (report == nil) { + BITHockeyLogWarning(@"WARNING: Could not parse crash report"); + } else { + NSDate *appStartTime = nil; + NSDate *appCrashTime = nil; + if ([report.processInfo respondsToSelector:@selector(processStartTime)]) { + if (report.systemInfo.timestamp && report.processInfo.processStartTime) { + appStartTime = report.processInfo.processStartTime; + appCrashTime =report.systemInfo.timestamp; + self.timeIntervalCrashInLastSessionOccurred = [report.systemInfo.timestamp timeIntervalSinceDate:report.processInfo.processStartTime]; + } + } + + [crashData writeToFile:[self.crashesDir stringByAppendingPathComponent: cacheFilename] atomically:YES]; + + NSString *incidentIdentifier = @"???"; + if (report.uuidRef != NULL) { + incidentIdentifier = (NSString *) CFBridgingRelease(CFUUIDCreateString(NULL, report.uuidRef)); + } + + NSString *reporterKey = bit_appAnonID(NO) ?: @""; + + self.lastSessionCrashDetails = [[BITCrashDetails alloc] initWithIncidentIdentifier:incidentIdentifier + reporterKey:reporterKey + signal:report.signalInfo.name + exceptionName:report.exceptionInfo.exceptionName + exceptionReason:report.exceptionInfo.exceptionReason + appStartTime:appStartTime + crashTime:appCrashTime + osVersion:report.systemInfo.operatingSystemVersion + osBuild:report.systemInfo.operatingSystemBuild + appVersion:report.applicationInfo.applicationMarketingVersion + appBuild:report.applicationInfo.applicationVersion + appProcessIdentifier:report.processInfo.processID + ]; + + // fetch and store the meta data after setting _lastSessionCrashDetails, so the property can be used in the protocol methods + [self storeMetaDataForCrashReportFilename:cacheFilename]; + } + } + } else { + BITHockeyLogWarning(@"WARNING: AnalyzerInProgress file found, handling crash report skipped"); + } + + // Purge the report + // mark the end of the routine + if ([self.fileManager fileExistsAtPath:self.analyzerInProgressFile]) { + [self.fileManager removeItemAtPath:self.analyzerInProgressFile error:&error]; + } + + [self saveSettings]; + + [self.plCrashReporter purgePendingCrashReport]; +} + +/** + Get the filename of the first not approved crash report + + @return NSString Filename of the first found not approved crash report + */ +- (NSString *)firstNotApprovedCrashReport { + if ((!self.approvedCrashReports || [self.approvedCrashReports count] == 0) && [self.crashFiles count] > 0) { + return [self.crashFiles objectAtIndex:0]; + } + + for (NSUInteger i=0; i < [self.crashFiles count]; i++) { + NSString *filename = [self.crashFiles objectAtIndex:i]; + + if (![self.approvedCrashReports objectForKey:filename]) return filename; + } + + return nil; +} + +/** + * Check if there are any new crash reports that are not yet processed + * + * @return `YES` if there is at least one new crash report found, `NO` otherwise + */ +- (BOOL)hasPendingCrashReport { + if (self.crashManagerStatus == BITCrashManagerStatusDisabled) return NO; + + if ([self.fileManager fileExistsAtPath:self.crashesDir]) { + NSError *error = NULL; + + NSArray *dirArray = [self.fileManager contentsOfDirectoryAtPath:self.crashesDir error:&error]; + + for (NSString *file in dirArray) { + NSString *filePath = [self.crashesDir stringByAppendingPathComponent:file]; + + NSDictionary *fileAttributes = [self.fileManager attributesOfItemAtPath:filePath error:&error]; + if ([[fileAttributes objectForKey:NSFileType] isEqualToString:NSFileTypeRegular] && + [[fileAttributes objectForKey:NSFileSize] intValue] > 0 && + ![file hasSuffix:@".DS_Store"] && + ![file hasSuffix:@".analyzer"] && + ![file hasSuffix:@".plist"] && + ![file hasSuffix:@".data"] && + ![file hasSuffix:@".meta"] && + ![file hasSuffix:@".desc"]) { + [self.crashFiles addObject:filePath]; + } + } + } + + if ([self.crashFiles count] > 0) { + BITHockeyLogDebug(@"INFO: %lu pending crash reports found.", (unsigned long)[self.crashFiles count]); + return YES; + } else { + if (self.didCrashInLastSession) { + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { + [strongDelegate crashManagerWillCancelSendingCrashReport:self]; + } + + self.didCrashInLastSession = NO; + } + + return NO; + } +} + + +#pragma mark - Crash Report Processing + +// store the latest crash report as user approved, so if it fails it will retry automatically +- (void)approveLatestCrashReport { + [self.approvedCrashReports setObject:[NSNumber numberWithBool:YES] forKey:[self.crashesDir stringByAppendingPathComponent: self.lastCrashFilename]]; + [self saveSettings]; +} + +- (void)triggerDelayedProcessing { + BITHockeyLogVerbose(@"VERBOSE: Triggering delayed crash processing."); + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(invokeDelayedProcessing) object:nil]; + [self performSelector:@selector(invokeDelayedProcessing) withObject:nil afterDelay:0.5]; +} + +/** + * Delayed startup processing for everything that does not to be done in the app startup runloop + * + * - Checks if there is another exception handler installed that may block ours + * - Present UI if the user has to approve new crash reports + * - Send pending approved crash reports + */ +- (void)invokeDelayedProcessing { +#if !defined (HOCKEYSDK_CONFIGURATION_ReleaseCrashOnlyExtensions) + if (!bit_isRunningInAppExtension() && [BITHockeyHelper applicationState] != BITApplicationStateActive) { + return; + } +#endif + + BITHockeyLogDebug(@"INFO: Start delayed CrashManager processing"); + + // was our own exception handler successfully added? + if (self.exceptionHandler) { + // get the current top level error handler + NSUncaughtExceptionHandler *currentHandler = NSGetUncaughtExceptionHandler(); + + // If the top level error handler differs from our own, then at least another one was added. + // This could cause exception crashes not to be reported to HockeyApp. See log message for details. + if (self.exceptionHandler != currentHandler) { + BITHockeyLogWarning(@"[HockeySDK] WARNING: Another exception handler was added. If this invokes any kind of exit() after processing the exception, which causes any subsequent error handler not to be invoked, these crashes will NOT be reported to HockeyApp!"); + } + } + + if (!self.sendingInProgress && [self hasPendingCrashReport]) { + self.sendingInProgress = YES; + + NSString *notApprovedReportFilename = [self firstNotApprovedCrashReport]; + + // this can happen in case there is a non approved crash report but it didn't happen in the previous app session + if (notApprovedReportFilename && !self.lastCrashFilename) { + self.lastCrashFilename = [notApprovedReportFilename lastPathComponent]; + } + + if (!BITHockeyBundle() || bit_isRunningInAppExtension()) { + [self approveLatestCrashReport]; + [self sendNextCrashReport]; + +#if !defined (HOCKEYSDK_CONFIGURATION_ReleaseCrashOnlyExtensions) + + } else if (self.crashManagerStatus != BITCrashManagerStatusAutoSend && notApprovedReportFilename) { + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(crashManagerWillShowSubmitCrashReportAlert:)]) { + [strongDelegate crashManagerWillShowSubmitCrashReportAlert:self]; + } + + NSString *appName = bit_appName(BITHockeyLocalizedString(@"HockeyAppNamePlaceholder")); + NSString *alertDescription = [NSString stringWithFormat:BITHockeyLocalizedString(@"CrashDataFoundAnonymousDescription"), appName]; + + // the crash report is not anonymous any more if username or useremail are not nil + NSString *userid = [self userIDForCrashReport]; + NSString *username = [self userNameForCrashReport]; + NSString *useremail = [self userEmailForCrashReport]; + + if ((userid && [userid length] > 0) || + (username && [username length] > 0) || + (useremail && [useremail length] > 0)) { + alertDescription = [NSString stringWithFormat:BITHockeyLocalizedString(@"CrashDataFoundDescription"), appName]; + } + + if (self.alertViewHandler) { + self.alertViewHandler(); + } else { + __weak typeof(self) weakSelf = self; + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:BITHockeyLocalizedString(@"CrashDataFoundTitle"), appName] + message:alertDescription + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *cancelAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"CrashDontSendReport") + style:UIAlertActionStyleCancel + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + [strongSelf handleUserInput:BITCrashManagerUserInputDontSend withUserProvidedMetaData:nil]; + }]; + [alertController addAction:cancelAction]; + UIAlertAction *sendAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"CrashSendReport") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + [strongSelf handleUserInput:BITCrashManagerUserInputSend withUserProvidedMetaData:nil]; + }]; + [alertController addAction:sendAction]; + if (self.shouldShowAlwaysButton) { + UIAlertAction *alwaysSendAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"CrashSendReportAlways") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + [strongSelf handleUserInput:BITCrashManagerUserInputAlwaysSend withUserProvidedMetaData:nil]; + }]; + + [alertController addAction:alwaysSendAction]; + } + + [self showAlertController:alertController]; + } +#endif /* !defined (HOCKEYSDK_CONFIGURATION_ReleaseCrashOnlyExtensions) */ + + } else { + [self approveLatestCrashReport]; + [self sendNextCrashReport]; + } + } +} + +/** + * Main startup sequence initializing PLCrashReporter if it wasn't disabled + */ +- (void)startManager { + if (self.crashManagerStatus == BITCrashManagerStatusDisabled) return; + + [self registerObservers]; + + [self loadSettings]; + + if (!self.isSetup) { + static dispatch_once_t plcrPredicate; + dispatch_once(&plcrPredicate, ^{ + /* Configure our reporter */ + + PLCrashReporterSignalHandlerType signalHandlerType = PLCrashReporterSignalHandlerTypeBSD; + if (self.isMachExceptionHandlerEnabled) { + signalHandlerType = PLCrashReporterSignalHandlerTypeMach; + } + + PLCrashReporterSymbolicationStrategy symbolicationStrategy = PLCrashReporterSymbolicationStrategyNone; + if (self.isOnDeviceSymbolicationEnabled) { + symbolicationStrategy = PLCrashReporterSymbolicationStrategyAll; + } + + BITPLCrashReporterConfig *config = [[BITPLCrashReporterConfig alloc] initWithSignalHandlerType: signalHandlerType + symbolicationStrategy: symbolicationStrategy]; + self.plCrashReporter = [[BITPLCrashReporter alloc] initWithConfiguration: config]; + + // Check if we previously crashed + if ([self.plCrashReporter hasPendingCrashReport]) { + self.didCrashInLastSession = YES; + [self handleCrashReport]; + } + + // The actual signal and mach handlers are only registered when invoking `enableCrashReporterAndReturnError` + // So it is safe enough to only disable the following part when a debugger is attached no matter which + // signal handler type is set + // We only check for this if we are not in the App Store environment + + BOOL debuggerIsAttached = NO; + if (self.appEnvironment != BITEnvironmentAppStore) { + if ([self isDebuggerAttached]) { + debuggerIsAttached = YES; + BITHockeyLogWarning(@"[HockeySDK] WARNING: Detecting crashes is NOT enabled due to running the app with a debugger attached."); + } + } + + if (!debuggerIsAttached) { + // Multiple exception handlers can be set, but we can only query the top level error handler (uncaught exception handler). + // + // To check if PLCrashReporter's error handler is successfully added, we compare the top + // level one that is set before and the one after PLCrashReporter sets up its own. + // + // With delayed processing we can then check if another error handler was set up afterwards + // and can show a debug warning log message, that the dev has to make sure the "newer" error handler + // doesn't exit the process itself, because then all subsequent handlers would never be invoked. + // + // Note: ANY error handler setup BEFORE HockeySDK initialization will not be processed! + + // get the current top level error handler + NSUncaughtExceptionHandler *initialHandler = NSGetUncaughtExceptionHandler(); + + // PLCrashReporter may only be initialized once. So make sure the developer + // can't break this + NSError *error = NULL; + +#if HOCKEYSDK_FEATURE_METRICS + [self configDefaultCrashCallback]; +#endif + // Set plCrashReporter callback which contains our default callback and potentially user defined callbacks + [self.plCrashReporter setCrashCallbacks:&plCrashCallbacks]; + + // Enable the Crash Reporter + if (![self.plCrashReporter enableCrashReporterAndReturnError: &error]) + BITHockeyLogError(@"[HockeySDK] ERROR: Could not enable crash reporter: %@", [error localizedDescription]); + + // get the new current top level error handler, which should now be the one from PLCrashReporter + NSUncaughtExceptionHandler *currentHandler = NSGetUncaughtExceptionHandler(); + + // do we have a new top level error handler? then we were successful + if (currentHandler && currentHandler != initialHandler) { + self.exceptionHandler = currentHandler; + + BITHockeyLogDebug(@"INFO: Exception handler successfully initialized."); + } else { + + // If we're running in a Xamarin Environment, the exception handler will be the one by the xamarin runtime, not ours. + // In other cases, this should never happen, theoretically only if NSSetUncaugtExceptionHandler() has some internal issues + BITHockeyLogError(@"[HockeySDK] ERROR: Exception handler could not be set. Make sure there is no other exception handler set up!"); + BITHockeyLogError(@"[HockeySDK] ERROR: If you are using the HockeySDK-Xamarin, this is expected behavior and you can ignore this message"); + } + + // Add the C++ uncaught exception handler, which is currently not handled by PLCrashReporter internally + [BITCrashUncaughtCXXExceptionHandlerManager addCXXExceptionHandler:uncaught_cxx_exception_handler]; + } + self.isSetup = YES; + }); + } + + if ([[NSUserDefaults standardUserDefaults] valueForKey:kBITAppDidReceiveLowMemoryNotification]) + self.didReceiveMemoryWarningInLastSession = [[NSUserDefaults standardUserDefaults] boolForKey:kBITAppDidReceiveLowMemoryNotification]; + + if (!self.didCrashInLastSession && self.isAppNotTerminatingCleanlyDetectionEnabled) { + BOOL didAppSwitchToBackgroundSafely = YES; + + if ([[NSUserDefaults standardUserDefaults] valueForKey:kBITAppWentIntoBackgroundSafely]) + didAppSwitchToBackgroundSafely = [[NSUserDefaults standardUserDefaults] boolForKey:kBITAppWentIntoBackgroundSafely]; + + if (!didAppSwitchToBackgroundSafely) { + BOOL considerReport = YES; + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(considerAppNotTerminatedCleanlyReportForCrashManager:)]) { + considerReport = [strongDelegate considerAppNotTerminatedCleanlyReportForCrashManager:self]; + } + + if (considerReport) { + BITHockeyLogVerbose(@"INFO: App kill detected, creating crash report."); + [self createCrashReportForAppKill]; + + self.didCrashInLastSession = YES; + } + } + } + +#if !defined (HOCKEYSDK_CONFIGURATION_ReleaseCrashOnlyExtensions) + if ([BITHockeyHelper applicationState] != BITApplicationStateActive) { + [self appEnteredForeground]; + } +#else + [self appEnteredForeground]; +#endif + + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kBITAppDidReceiveLowMemoryNotification]; + + [self triggerDelayedProcessing]; + BITHockeyLogVerbose(@"VERBOSE: CrashManager startManager has finished."); +} + +/** + * Creates a fake crash report because the app was killed while being in foreground + */ +- (void)createCrashReportForAppKill { + NSString *fakeReportUUID = bit_UUID(); + NSString *fakeReporterKey = bit_appAnonID(NO) ?: @"???"; + + NSString *fakeReportAppMarketingVersion = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppMarketingVersion]; + + NSString *fakeReportAppVersion = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppVersion]; + if (!fakeReportAppVersion) + return; + + NSString *fakeReportOSVersion = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppOSVersion] ?: [[UIDevice currentDevice] systemVersion]; + + NSString *fakeReportOSVersionString = fakeReportOSVersion; + NSString *fakeReportOSBuild = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppOSBuild] ?: [self osBuild]; + if (fakeReportOSBuild) { + fakeReportOSVersionString = [NSString stringWithFormat:@"%@ (%@)", fakeReportOSVersion, fakeReportOSBuild]; + } + + NSString *fakeReportAppBundleIdentifier = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; + NSString *fakeReportDeviceModel = [self getDevicePlatform] ?: @"Unknown"; + NSString *fakeReportAppUUIDs = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppUUIDs] ?: @""; + + NSString *fakeSignalName = kBITCrashKillSignal; + + NSMutableString *fakeReportString = [NSMutableString string]; + + [fakeReportString appendFormat:@"Incident Identifier: %@\n", fakeReportUUID]; + [fakeReportString appendFormat:@"CrashReporter Key: %@\n", fakeReporterKey]; + [fakeReportString appendFormat:@"Hardware Model: %@\n", fakeReportDeviceModel]; + [fakeReportString appendFormat:@"Identifier: %@\n", fakeReportAppBundleIdentifier]; + + NSString *fakeReportAppVersionString = fakeReportAppMarketingVersion ? [NSString stringWithFormat:@"%@ (%@)", fakeReportAppMarketingVersion, fakeReportAppVersion] : fakeReportAppVersion; + + [fakeReportString appendFormat:@"Version: %@\n", fakeReportAppVersionString]; + [fakeReportString appendString:@"Code Type: ARM\n"]; + [fakeReportString appendString:@"\n"]; + + NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + NSDateFormatter *rfc3339Formatter = [[NSDateFormatter alloc] init]; + [rfc3339Formatter setLocale:enUSPOSIXLocale]; + [rfc3339Formatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"]; + [rfc3339Formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + NSString *fakeCrashTimestamp = [rfc3339Formatter stringFromDate:[NSDate date]]; + + // we use the current date, since we don't know when the kill actually happened + [fakeReportString appendFormat:@"Date/Time: %@\n", fakeCrashTimestamp]; + [fakeReportString appendFormat:@"OS Version: %@\n", fakeReportOSVersionString]; + [fakeReportString appendString:@"Report Version: 104\n"]; + [fakeReportString appendString:@"\n"]; + [fakeReportString appendFormat:@"Exception Type: %@\n", fakeSignalName]; + [fakeReportString appendString:@"Exception Codes: 00000020 at 0x8badf00d\n"]; + [fakeReportString appendString:@"\n"]; + [fakeReportString appendString:@"Application Specific Information:\n"]; + [fakeReportString appendString:@"The application did not terminate cleanly but no crash occured."]; + if (self.didReceiveMemoryWarningInLastSession) { + [fakeReportString appendString:@" The app received at least one Low Memory Warning."]; + } + [fakeReportString appendString:@"\n\n"]; + + NSString *fakeReportFilename = [NSString stringWithFormat: @"%.0f", [NSDate timeIntervalSinceReferenceDate]]; + + NSError *error = nil; + + NSMutableDictionary *rootObj = [NSMutableDictionary dictionaryWithCapacity:2]; + [rootObj setObject:fakeReportUUID forKey:kBITFakeCrashUUID]; + if (fakeReportAppMarketingVersion) + [rootObj setObject:fakeReportAppMarketingVersion forKey:kBITFakeCrashAppMarketingVersion]; + [rootObj setObject:fakeReportAppVersion forKey:kBITFakeCrashAppVersion]; + [rootObj setObject:fakeReportAppBundleIdentifier forKey:kBITFakeCrashAppBundleIdentifier]; + [rootObj setObject:fakeReportOSVersion forKey:kBITFakeCrashOSVersion]; + [rootObj setObject:fakeReportDeviceModel forKey:kBITFakeCrashDeviceModel]; + [rootObj setObject:fakeReportAppUUIDs forKey:kBITFakeCrashAppBinaryUUID]; + [rootObj setObject:fakeReportString forKey:kBITFakeCrashReport]; + + self.lastSessionCrashDetails = [[BITCrashDetails alloc] initWithIncidentIdentifier:fakeReportUUID + reporterKey:fakeReporterKey + signal:fakeSignalName + exceptionName:nil + exceptionReason:nil + appStartTime:nil + crashTime:nil + osVersion:fakeReportOSVersion + osBuild:fakeReportOSBuild + appVersion:fakeReportAppMarketingVersion + appBuild:fakeReportAppVersion + appProcessIdentifier:[[NSProcessInfo processInfo] processIdentifier] + ]; + + NSData *plist = [NSPropertyListSerialization dataWithPropertyList:(id)rootObj + format:NSPropertyListBinaryFormat_v1_0 + options:0 + error:&error]; + if (plist) { + if ([plist writeToFile:[self.crashesDir stringByAppendingPathComponent:(NSString *)[fakeReportFilename stringByAppendingPathExtension:@"fake"]] atomically:YES]) { + [self storeMetaDataForCrashReportFilename:fakeReportFilename]; + } + } else { + BITHockeyLogError(@"ERROR: Writing fake crash report. %@", [error description]); + } +} + +/** + * Send all approved crash reports + * + * Gathers all collected data and constructs the XML structure and starts the sending process + */ +- (void)sendNextCrashReport { + NSError *error = NULL; + + self.crashIdenticalCurrentVersion = NO; + + if ([self.crashFiles count] == 0) + return; + + NSString *crashXML = nil; + BITHockeyAttachment *attachment = nil; + + // we start sending always with the oldest pending one + NSString *filename = [self.crashFiles objectAtIndex:0]; + NSString *attachmentFilename = filename; + NSString *cacheFilename = [filename lastPathComponent]; + NSData *crashData = [NSData dataWithContentsOfFile:filename]; + + if ([crashData length] > 0) { + BITPLCrashReport *report = nil; + NSString *crashUUID = @""; + NSString *installString = nil; + NSString *crashLogString = nil; + NSString *appBundleIdentifier = nil; + NSString *appBundleMarketingVersion = nil; + NSString *appBundleVersion = nil; + NSString *osVersion = nil; + NSString *deviceModel = nil; + NSString *appBinaryUUIDs = nil; + NSString *metaFilename = nil; + + NSPropertyListFormat format; + + if ([[cacheFilename pathExtension] isEqualToString:@"fake"]) { + NSDictionary *fakeReportDict = (NSDictionary *)[NSPropertyListSerialization + propertyListWithData:crashData + options:NSPropertyListMutableContainersAndLeaves + format:&format + error:&error]; + + crashLogString = [fakeReportDict objectForKey:kBITFakeCrashReport]; + crashUUID = [fakeReportDict objectForKey:kBITFakeCrashUUID]; + appBundleIdentifier = [fakeReportDict objectForKey:kBITFakeCrashAppBundleIdentifier]; + appBundleMarketingVersion = [fakeReportDict objectForKey:kBITFakeCrashAppMarketingVersion] ?: @""; + appBundleVersion = [fakeReportDict objectForKey:kBITFakeCrashAppVersion]; + appBinaryUUIDs = [fakeReportDict objectForKey:kBITFakeCrashAppBinaryUUID]; + deviceModel = [fakeReportDict objectForKey:kBITFakeCrashDeviceModel]; + osVersion = [fakeReportDict objectForKey:kBITFakeCrashOSVersion]; + + metaFilename = [cacheFilename stringByReplacingOccurrencesOfString:@".fake" withString:@".meta"]; + attachmentFilename = [attachmentFilename stringByReplacingOccurrencesOfString:@".fake" withString:@""]; + + if ([appBundleVersion compare:(NSString *)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { + self.crashIdenticalCurrentVersion = YES; + } + + } else { + report = [[BITPLCrashReport alloc] initWithData:crashData error:&error]; + } + + if (report == nil && crashLogString == nil) { + BITHockeyLogWarning(@"WARNING: Could not parse crash report"); + // we cannot do anything with this report, so delete it + [self cleanCrashReportWithFilename:filename]; + // we don't continue with the next report here, even if there are to prevent calling sendCrashReports from itself again + // the next crash will be automatically send on the next app start/becoming active event + return; + } + + installString = bit_appAnonID(NO) ?: @""; + + if (report) { + if (report.uuidRef != NULL) { + crashUUID = (NSString *) CFBridgingRelease(CFUUIDCreateString(NULL, report.uuidRef)); + } + metaFilename = [cacheFilename stringByAppendingPathExtension:@"meta"]; + crashLogString = [BITCrashReportTextFormatter stringValueForCrashReport:report crashReporterKey:installString]; + appBundleIdentifier = report.applicationInfo.applicationIdentifier; + appBundleMarketingVersion = report.applicationInfo.applicationMarketingVersion ?: @""; + appBundleVersion = report.applicationInfo.applicationVersion; + osVersion = report.systemInfo.operatingSystemVersion; + deviceModel = [self getDevicePlatform]; + appBinaryUUIDs = [self extractAppUUIDs:report]; + if ([report.applicationInfo.applicationVersion compare:(NSString *)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { + self.crashIdenticalCurrentVersion = YES; + } + } + + if ([report.applicationInfo.applicationVersion compare:(NSString *)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { + self.crashIdenticalCurrentVersion = YES; + } + + NSString *username = @""; + NSString *useremail = @""; + NSString *userid = @""; + NSString *applicationLog = @""; + NSString *description = @""; + + NSData *plist = [NSData dataWithContentsOfFile:[self.crashesDir stringByAppendingPathComponent:metaFilename]]; + if (plist) { + NSDictionary *metaDict = (NSDictionary *)[NSPropertyListSerialization + propertyListWithData:plist + options:NSPropertyListMutableContainersAndLeaves + format:&format + error:&error]; + + username = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", attachmentFilename.lastPathComponent, kBITCrashMetaUserName]] ?: @""; + useremail = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", attachmentFilename.lastPathComponent, kBITCrashMetaUserEmail]] ?: @""; + userid = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", attachmentFilename.lastPathComponent, kBITCrashMetaUserID]] ?: @""; + applicationLog = [metaDict objectForKey:kBITCrashMetaApplicationLog] ?: @""; + description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@.desc", [self.crashesDir stringByAppendingPathComponent: cacheFilename]] encoding:NSUTF8StringEncoding error:&error]; + attachment = [self attachmentForCrashReport:attachmentFilename]; + } else { + BITHockeyLogError(@"ERROR: Reading crash meta data. %@", error); + } + + if ([applicationLog length] > 0) { + if ([description length] > 0) { + description = [NSString stringWithFormat:@"%@\n\nLog:\n%@", description, applicationLog]; + } else { + description = [NSString stringWithFormat:@"Log:\n%@", applicationLog]; + } + } + + crashXML = [NSString stringWithFormat:@"%@%@%@%@%@%@%@%@%@%@%@%@", + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"], + appBinaryUUIDs, + appBundleIdentifier, + osVersion, + deviceModel, + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"], + appBundleMarketingVersion, + appBundleVersion, + crashUUID, + [crashLogString stringByReplacingOccurrencesOfString:@"]]>" withString:@"]]" @"]]>" options:NSLiteralSearch range:NSMakeRange(0,crashLogString.length)], + userid, + username, + useremail, + installString, + [description stringByReplacingOccurrencesOfString:@"]]>" withString:@"]]" @"]]>" options:NSLiteralSearch range:NSMakeRange(0,description.length)]]; + + BITHockeyLogDebug(@"INFO: Sending crash reports:\n%@", crashXML); + [self sendCrashReportWithFilename:filename xml:crashXML attachment:attachment]; + } else { + // we cannot do anything with this report, so delete it + [self cleanCrashReportWithFilename:filename]; + } +} + +#pragma mark - Networking + +- (NSData *)postBodyWithXML:(NSString *)xml attachment:(BITHockeyAttachment *)attachment boundary:(NSString *)boundary { + NSMutableData *postBody = [NSMutableData data]; + + // [postBody appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]]; + [postBody appendData:[BITHockeyAppClient dataWithPostValue:BITHOCKEY_NAME + forKey:@"sdk" + boundary:boundary]]; + + [postBody appendData:[BITHockeyAppClient dataWithPostValue:BITHOCKEY_VERSION + forKey:@"sdk_version" + boundary:boundary]]; + + [postBody appendData:[BITHockeyAppClient dataWithPostValue:@"no" + forKey:@"feedbackEnabled" + boundary:boundary]]; + + [postBody appendData:[BITHockeyAppClient dataWithPostValue:[xml dataUsingEncoding:NSUTF8StringEncoding] + forKey:@"xml" + contentType:@"text/xml" + boundary:boundary + filename:@"crash.xml"]]; + + if (attachment && attachment.hockeyAttachmentData) { + NSString *attachmentFilename = attachment.filename; + if (!attachmentFilename) { + attachmentFilename = @"Attachment_0"; + } + [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.hockeyAttachmentData + forKey:@"attachment0" + contentType:attachment.contentType + boundary:boundary + filename:attachmentFilename]]; + } + + [postBody appendData:(NSData *)[[NSString stringWithFormat:@"\r\n--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + + return postBody; +} + +- (NSMutableURLRequest *)requestWithBoundary:(NSString *)boundary { + NSString *postCrashPath = [NSString stringWithFormat:@"api/2/apps/%@/crashes", self.encodedAppIdentifier]; + + NSMutableURLRequest *request = [self.hockeyAppClient requestWithMethod:@"POST" + path:postCrashPath + parameters:nil]; + + [request setCachePolicy: NSURLRequestReloadIgnoringLocalCacheData]; + [request setValue:@"HockeySDK/iOS" forHTTPHeaderField:@"User-Agent"]; + [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"]; + + NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; + [request setValue:contentType forHTTPHeaderField:@"Content-type"]; + + return request; +} + +// process upload response +- (void)processUploadResultWithFilename:(NSString *)filename responseData:(NSData *)responseData statusCode:(NSInteger)statusCode error:(NSError *)error { + __block NSError *theError = error; + + dispatch_async(dispatch_get_main_queue(), ^{ + self.sendingInProgress = NO; + id strongDelegate = self.delegate; + if (nil == theError) { + if (nil == responseData || [responseData length] == 0) { + theError = [NSError errorWithDomain:kBITCrashErrorDomain + code:BITCrashAPIReceivedEmptyResponse + userInfo:@{ + NSLocalizedDescriptionKey: @"Sending failed with an empty response!" + } + ]; + } else if (statusCode >= 200 && statusCode < 400) { + [self cleanCrashReportWithFilename:filename]; + + // HockeyApp uses PList XML format + NSMutableDictionary *response = [NSPropertyListSerialization propertyListWithData:responseData + options:NSPropertyListMutableContainersAndLeaves + format:nil + error:&theError]; + BITHockeyLogDebug(@"INFO: Received API response: %@", response); + if ([strongDelegate respondsToSelector:@selector(crashManagerDidFinishSendingCrashReport:)]) { + [strongDelegate crashManagerDidFinishSendingCrashReport:self]; + } + + // only if sending the crash report went successfully, continue with the next one (if there are more) + [self sendNextCrashReport]; + } else if (statusCode == 400) { + [self cleanCrashReportWithFilename:filename]; + + theError = [NSError errorWithDomain:kBITCrashErrorDomain + code:BITCrashAPIAppVersionRejected + userInfo:@{ + NSLocalizedDescriptionKey: @"The server rejected receiving crash reports for this app version!" + } + ]; + } else { + theError = [NSError errorWithDomain:kBITCrashErrorDomain + code:BITCrashAPIErrorWithStatusCode + userInfo:@{ + NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Sending failed with status code: %li", (long)statusCode] + } + ]; + } + } + + if (theError) { + if ([strongDelegate respondsToSelector:@selector(crashManager:didFailWithError:)]) { + [strongDelegate crashManager:self didFailWithError:theError]; + } + + BITHockeyLogError(@"ERROR: %@", [theError localizedDescription]); + } + }); +} + +/** + * Send the XML data to the server + * + * Wraps the XML structure into a POST body and starts sending the data asynchronously + * + * @param xml The XML data that needs to be send to the server + */ +- (void)sendCrashReportWithFilename:(NSString *)filename xml:(NSString*)xml attachment:(BITHockeyAttachment *)attachment { + NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + __block NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; + + NSURLRequest *request = [self requestWithBoundary:kBITHockeyAppClientBoundary]; + NSData *data = [self postBodyWithXML:xml attachment:attachment boundary:kBITHockeyAppClientBoundary]; + + if (request && data) { + __weak typeof (self) weakSelf = self; + NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request + fromData:data + completionHandler:^(NSData *responseData, NSURLResponse *response, NSError *error) { + typeof (self) strongSelf = weakSelf; + + [session finishTasksAndInvalidate]; + + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*) response; + NSInteger statusCode = [httpResponse statusCode]; + [strongSelf processUploadResultWithFilename:filename responseData:responseData statusCode:statusCode error:error]; + }]; + + [uploadTask resume]; + } + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(crashManagerWillSendCrashReport:)]) { + [strongDelegate crashManagerWillSendCrashReport:self]; + } + + BITHockeyLogDebug(@"INFO: Sending crash reports started."); +} + +- (NSTimeInterval)timeintervalCrashInLastSessionOccured { + return self.timeIntervalCrashInLastSessionOccurred; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ + diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashManagerDelegate.h b/submodules/HockeySDK-iOS/Classes/BITCrashManagerDelegate.h new file mode 100644 index 0000000000..786ead9d22 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashManagerDelegate.h @@ -0,0 +1,155 @@ +/* + * 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 + +@class BITCrashManager; +@class BITHockeyAttachment; + +/** + The `BITCrashManagerDelegate` formal protocol defines methods further configuring + the behaviour of `BITCrashManager`. + */ + +@protocol BITCrashManagerDelegate + +@optional + + +///----------------------------------------------------------------------------- +/// @name Additional meta data +///----------------------------------------------------------------------------- + +/** Return any log string based data the crash report being processed should contain + + @param crashManager The `BITCrashManager` instance invoking this delegate + @see attachmentForCrashManager: + @see BITHockeyManagerDelegate userNameForHockeyManager:componentManager: + @see BITHockeyManagerDelegate userEmailForHockeyManager:componentManager: + */ +-(NSString *)applicationLogForCrashManager:(BITCrashManager *)crashManager; + + +/** Return a BITHockeyAttachment object providing an NSData object the crash report + being processed should contain + + Please limit your attachments to reasonable files to avoid high traffic costs for your users. + + Example implementation: + + - (BITHockeyAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManager { + NSData *data = [NSData dataWithContentsOfURL:@"mydatafile"]; + + BITHockeyAttachment *attachment = [[BITHockeyAttachment alloc] initWithFilename:@"myfile.data" + hockeyAttachmentData:data + contentType:@"'application/octet-stream"]; + return attachment; + } + + @param crashManager The `BITCrashManager` instance invoking this delegate + @see BITHockeyAttachment + @see applicationLogForCrashManager: + @see BITHockeyManagerDelegate userNameForHockeyManager:componentManager: + @see BITHockeyManagerDelegate userEmailForHockeyManager:componentManager: + */ +-(BITHockeyAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManager; + + + +///----------------------------------------------------------------------------- +/// @name Alert +///----------------------------------------------------------------------------- + +/** Invoked before the user is asked to send a crash report, so you can do additional actions. + E.g. to make sure not to ask the user for an app rating :) + + @param crashManager The `BITCrashManager` instance invoking this delegate + */ +-(void)crashManagerWillShowSubmitCrashReportAlert:(BITCrashManager *)crashManager; + + +/** Invoked after the user did choose _NOT_ to send a crash in the alert + + @param crashManager The `BITCrashManager` instance invoking this delegate + */ +-(void)crashManagerWillCancelSendingCrashReport:(BITCrashManager *)crashManager; + + +/** Invoked after the user did choose to send crashes always in the alert + + @param crashManager The `BITCrashManager` instance invoking this delegate + */ +-(void)crashManagerWillSendCrashReportsAlways:(BITCrashManager *)crashManager; + + +///----------------------------------------------------------------------------- +/// @name Networking +///----------------------------------------------------------------------------- + +/** Invoked right before sending crash reports will start + + @param crashManager The `BITCrashManager` instance invoking this delegate + */ +- (void)crashManagerWillSendCrashReport:(BITCrashManager *)crashManager; + +/** Invoked after sending crash reports failed + + @param crashManager The `BITCrashManager` instance invoking this delegate + @param error The error returned from the NSURLSession call or `kBITCrashErrorDomain` + with reason of type `BITCrashErrorReason`. + */ +- (void)crashManager:(BITCrashManager *)crashManager didFailWithError:(NSError *)error; + +/** Invoked after sending crash reports succeeded + + @param crashManager The `BITCrashManager` instance invoking this delegate + */ +- (void)crashManagerDidFinishSendingCrashReport:(BITCrashManager *)crashManager; + +///----------------------------------------------------------------------------- +/// @name Experimental +///----------------------------------------------------------------------------- + +/** Define if a report should be considered as a crash report + + Due to the risk, that these reports may be false positives, this delegates allows the + developer to influence which reports detected by the heuristic should actually be reported. + + The developer can use the following property to get more information about the crash scenario: + - `[BITCrashManager didReceiveMemoryWarningInLastSession]`: Did the app receive a low memory warning + + This allows only reports to be considered where at least one low memory warning notification was + received by the app to reduce to possibility of having false positives. + + @param crashManager The `BITCrashManager` instance invoking this delegate + @return `YES` if the heuristic based detected report should be reported, otherwise `NO` + @see `[BITCrashManager didReceiveMemoryWarningInLastSession]` + */ +-(BOOL)considerAppNotTerminatedCleanlyReportForCrashManager:(BITCrashManager *)crashManager; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashManagerPrivate.h b/submodules/HockeySDK-iOS/Classes/BITCrashManagerPrivate.h new file mode 100644 index 0000000000..0add0c801d --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashManagerPrivate.h @@ -0,0 +1,113 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2013-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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER + +#import + +@class BITHockeyAppClient; + +@interface BITCrashManager () { +} + + +///----------------------------------------------------------------------------- +/// @name Delegate +///----------------------------------------------------------------------------- + +/** + Sets the optional `BITCrashManagerDelegate` delegate. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see `[BITHockeyManager setDelegate:]` + */ +@property (nonatomic, weak) id delegate; + +/** + * must be set + */ +@property (nonatomic, strong) BITHockeyAppClient *hockeyAppClient; + +@property (nonatomic) NSUncaughtExceptionHandler *exceptionHandler; + +@property (nonatomic, strong) NSFileManager *fileManager; + +@property (nonatomic, strong) BITPLCrashReporter *plCrashReporter; + +@property (nonatomic) NSString *lastCrashFilename; + +@property (nonatomic, copy, setter = setAlertViewHandler:) BITCustomAlertViewHandler alertViewHandler; + +@property (nonatomic, copy) NSString *crashesDir; + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + +// Only set via BITAuthenticator +@property (nonatomic, copy) NSString *installationIdentification; + +// Only set via BITAuthenticator +@property (nonatomic) BITAuthenticatorIdentificationType installationIdentificationType; + +// Only set via BITAuthenticator +@property (nonatomic) BOOL installationIdentified; + +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ + +- (instancetype)initWithAppIdentifier:(NSString *)appIdentifier appEnvironment:(BITEnvironment)environment hockeyAppClient:(BITHockeyAppClient *)hockeyAppClient NS_DESIGNATED_INITIALIZER; + +- (void)cleanCrashReports; + +- (NSString *)userIDForCrashReport; +- (NSString *)userEmailForCrashReport; +- (NSString *)userNameForCrashReport; + +- (void)handleCrashReport; +- (BOOL)hasPendingCrashReport; +- (NSString *)firstNotApprovedCrashReport; + +- (void)persistUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData; +- (BOOL)persistAttachment:(BITHockeyAttachment *)attachment withFilename:(NSString *)filename; + +- (BITHockeyAttachment *)attachmentForCrashReport:(NSString *)filename; + +- (void)invokeDelayedProcessing; +- (void)sendNextCrashReport; + +- (void)setLastCrashFilename:(NSString *)lastCrashFilename; + +- (void)leavingAppSafely; + +@end + + +#endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashMetaData.h b/submodules/HockeySDK-iOS/Classes/BITCrashMetaData.h new file mode 100644 index 0000000000..10508b5794 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashMetaData.h @@ -0,0 +1,57 @@ +/* + * 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 + + +/** + * This class provides properties that can be attached to a crash report via a custom alert view flow + */ +@interface BITCrashMetaData : NSObject + +/** + * User provided description that should be attached to the crash report as plain text + */ +@property (nonatomic, copy) NSString *userProvidedDescription; + +/** + * User name that should be attached to the crash report + */ +@property (nonatomic, copy) NSString *userName; + +/** + * User email that should be attached to the crash report + */ +@property (nonatomic, copy) NSString *userEmail; + +/** + * User ID that should be attached to the crash report + */ +@property (nonatomic, copy) NSString *userID; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashMetaData.m b/submodules/HockeySDK-iOS/Classes/BITCrashMetaData.m new file mode 100644 index 0000000000..2ec133dd65 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashMetaData.m @@ -0,0 +1,39 @@ +/* + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER + +#import "BITCrashMetaData.h" + +@implementation BITCrashMetaData + +@end + +#endif diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashReportTextFormatter.h b/submodules/HockeySDK-iOS/Classes/BITCrashReportTextFormatter.h new file mode 100644 index 0000000000..c75d93c67f --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashReportTextFormatter.h @@ -0,0 +1,75 @@ +/* + * Authors: + * Landon Fuller + * Damian Morris + * Andreas Linde + * + * Copyright (c) 2008-2013 Plausible Labs Cooperative, Inc. + * Copyright (c) 2010 MOSO Corporation, Pty Ltd. + * 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 + +@class PLCrashReport; + + +// Dictionary keys for array elements returned by arrayOfAppUUIDsForCrashReport: +#ifndef kBITBinaryImageKeyUUID +#define kBITBinaryImageKeyUUID @"uuid" +#define kBITBinaryImageKeyArch @"arch" +#define kBITBinaryImageKeyType @"type" +#endif + + +/** + * HockeySDK Crash Reporter error domain + */ +typedef NS_ENUM (NSInteger, BITBinaryImageType) { + /** + * App binary + */ + BITBinaryImageTypeAppBinary, + /** + * App provided framework + */ + BITBinaryImageTypeAppFramework, + /** + * Image not related to the app + */ + BITBinaryImageTypeOther +}; + + +@interface BITCrashReportTextFormatter : NSObject { +} + ++ (NSString *)stringValueForCrashReport:(PLCrashReport *)report crashReporterKey:(NSString *)crashReporterKey; ++ (NSArray *)arrayOfAppUUIDsForCrashReport:(PLCrashReport *)report; ++ (NSString *)bit_archNameFromCPUType:(uint64_t)cpuType subType:(uint64_t)subType; ++ (BITBinaryImageType)bit_imageTypeForImagePath:(NSString *)imagePath processPath:(NSString *)processPath; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashReportTextFormatter.m b/submodules/HockeySDK-iOS/Classes/BITCrashReportTextFormatter.m new file mode 100644 index 0000000000..4c896802e6 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashReportTextFormatter.m @@ -0,0 +1,928 @@ +/* + * Authors: + * Landon Fuller + * Damian Morris + * Andreas Linde + * + * Copyright (c) 2008-2013 Plausible Labs Cooperative, Inc. + * Copyright (c) 2010 MOSO Corporation, Pty Ltd. + * 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 "HockeySDK.h" +#import "HockeySDKPrivate.h" + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER + +#import + +#import +#import +#import +#import +#import + +#if defined(__OBJC2__) +#define SEL_NAME_SECT "__objc_methname" +#else +#define SEL_NAME_SECT "__cstring" +#endif + +#import "BITCrashReportTextFormatter.h" + +/* + * XXX: The ARM64 CPU type, and ARM_V7S and ARM_V8 Mach-O CPU subtypes are not + * defined in the Mac OS X 10.8 headers. + */ +#ifndef CPU_SUBTYPE_ARM_V7S +# define CPU_SUBTYPE_ARM_V7S 11 +#endif + +#ifndef CPU_TYPE_ARM64 +#define CPU_TYPE_ARM64 (CPU_TYPE_ARM | CPU_ARCH_ABI64) +#endif + +#ifndef CPU_SUBTYPE_ARM_V8 +# define CPU_SUBTYPE_ARM_V8 13 +#endif + +/** + * Sort PLCrashReportBinaryImageInfo instances by their starting address. + */ +static NSInteger bit_binaryImageSort(id binary1, id binary2, void *__unused context) { + uint64_t addr1 = [binary1 imageBaseAddress]; + uint64_t addr2 = [binary2 imageBaseAddress]; + + if (addr1 < addr2) + return NSOrderedAscending; + else if (addr1 > addr2) + return NSOrderedDescending; + else + return NSOrderedSame; +} + +/** + * Validates that the given @a string terminates prior to @a limit. + */ +static const char *safer_string_read (const char *string, const char *limit) { + const char *p = string; + do { + if (p >= limit || p+1 >= limit) { + return NULL; + } + p++; + } while (*p != '\0'); + + return string; +} + +/* + * The relativeAddress should be ` - `, extracted from the crash report's thread + * and binary image list. + * + * For the (architecture-specific) registers to attempt, see: + * http://sealiesoftware.com/blog/archive/2008/09/22/objc_explain_So_you_crashed_in_objc_msgSend.html + */ +static const char *findSEL (const char *imageName, NSString *imageUUID, uint64_t relativeAddress) { + unsigned int images_count = _dyld_image_count(); + for (unsigned int i = 0; i < images_count; ++i) { + intptr_t slide = _dyld_get_image_vmaddr_slide(i); + const struct mach_header *header = _dyld_get_image_header(i); + const struct mach_header_64 *header64 = (const struct mach_header_64 *) header; + const char *name = _dyld_get_image_name(i); + + /* Image disappeared? */ + if (name == NULL || header == NULL) + continue; + + /* Check if this is the correct image. If we were being even more careful, we'd check the LC_UUID */ + if (strcmp(name, imageName) != 0) + continue; + + /* Determine whether this is a 64-bit or 32-bit Mach-O file */ + BOOL m64 = NO; + if (header->magic == MH_MAGIC_64) + m64 = YES; + + NSString *uuidString = nil; + const uint8_t *command; + uint32_t ncmds; + + if (m64) { + command = (const uint8_t *)(header64 + 1); + ncmds = header64->ncmds; + } else { + command = (const uint8_t *)(header + 1); + ncmds = header->ncmds; + } + for (uint32_t idx = 0; idx < ncmds; ++idx) { + const struct load_command *load_command = (const struct load_command *)command; + if (load_command->cmd == LC_UUID) { + const struct uuid_command *uuid_command = (const struct uuid_command *)command; + const uint8_t *uuid = uuid_command->uuid; + uuidString = [[NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", + uuid[0], uuid[1], uuid[2], uuid[3], + uuid[4], uuid[5], uuid[6], uuid[7], + uuid[8], uuid[9], uuid[10], uuid[11], + uuid[12], uuid[13], uuid[14], uuid[15]] + lowercaseString]; + break; + } else { + command += load_command->cmdsize; + } + } + + // Check if this is the correct image by comparing the UUIDs + if (!uuidString || ![uuidString isEqualToString:imageUUID]) + continue; + + /* Fetch the __objc_methname section */ + const char *methname_sect; + uint64_t methname_sect_size; + if (m64) { + methname_sect = getsectdatafromheader_64(header64, SEG_TEXT, SEL_NAME_SECT, &methname_sect_size); + } else { + uint32_t meth_size_32; + methname_sect = getsectdatafromheader(header, SEG_TEXT, SEL_NAME_SECT, &meth_size_32); + methname_sect_size = meth_size_32; + } + + /* Apply the slide, as per getsectdatafromheader(3) */ + methname_sect += slide; + + if (methname_sect == NULL) { + return NULL; + } + + /* Calculate the target address within this image, and verify that it is within __objc_methname */ + const char *target = ((const char *)header) + relativeAddress; + const char *limit = methname_sect + methname_sect_size; + if (target < methname_sect || target >= limit) { + return NULL; + } + + /* Read the actual method name */ + return safer_string_read(target, limit); + } + + return NULL; +} + +/** + * Formats PLCrashReport data as human-readable text. + */ +@implementation BITCrashReportTextFormatter + +static NSString *const BITXamarinStackTraceDelimiter = @"Xamarin Exception Stack:"; + +/** + * Formats the provided @a report as human-readable text in the given @a textFormat, and return + * the formatted result as a string. + * + * @param report The report to format. + * @param crashReporterKey The crash reporter key. + * + * @return Returns the formatted result on success, or nil if an error occurs. + */ ++ (NSString *)stringValueForCrashReport:(BITPLCrashReport *)report crashReporterKey:(NSString *)crashReporterKey { + NSMutableString* text = [NSMutableString string]; + boolean_t lp64 = true; // quiesce GCC uninitialized value warning + + /* Header */ + + /* Map to apple style OS name */ + NSString *osName; + switch (report.systemInfo.operatingSystem) { + case PLCrashReportOperatingSystemMacOSX: + osName = @"Mac OS X"; + break; + case PLCrashReportOperatingSystemiPhoneOS: + osName = @"iPhone OS"; + break; + case PLCrashReportOperatingSystemiPhoneSimulator: + osName = @"Mac OS X"; + break; + default: + osName = [NSString stringWithFormat: @"Unknown (%d)", report.systemInfo.operatingSystem]; + break; + } + + /* Map to Apple-style code type, and mark whether architecture is LP64 (64-bit) */ + NSString *codeType = nil; + { + /* Attempt to derive the code type from the binary images */ + for (BITPLCrashReportBinaryImageInfo *image in report.images) { + /* Skip images with no specified type */ + if (image.codeType == nil) + continue; + + /* Skip unknown encodings */ + if (image.codeType.typeEncoding != PLCrashReportProcessorTypeEncodingMach) + continue; + + switch (image.codeType.type) { + case CPU_TYPE_ARM: + codeType = @"ARM"; + lp64 = false; + break; + + case CPU_TYPE_ARM64: + codeType = @"ARM-64"; + lp64 = true; + break; + + case CPU_TYPE_X86: + codeType = @"X86"; + lp64 = false; + break; + + case CPU_TYPE_X86_64: + codeType = @"X86-64"; + lp64 = true; + break; + + case CPU_TYPE_POWERPC: + codeType = @"PPC"; + lp64 = false; + break; + + default: + // Do nothing, handled below. + break; + } + + /* Stop immediately if code type was discovered */ + if (codeType != nil) + break; + } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + /* If we were unable to determine the code type, fall back on the legacy architecture value. */ + if (codeType == nil) { + switch (report.systemInfo.architecture) { + case PLCrashReportArchitectureARMv6: + case PLCrashReportArchitectureARMv7: + codeType = @"ARM"; + lp64 = false; + break; + case PLCrashReportArchitectureX86_32: + codeType = @"X86"; + lp64 = false; + break; + case PLCrashReportArchitectureX86_64: + codeType = @"X86-64"; + lp64 = true; + break; + case PLCrashReportArchitecturePPC: + codeType = @"PPC"; + lp64 = false; + break; + default: + codeType = [NSString stringWithFormat: @"Unknown (%d)", report.systemInfo.architecture]; + lp64 = true; + break; + } + } +#pragma GCC diagnostic pop + } + + { + NSString *reporterKey = @"???"; + if (crashReporterKey && [crashReporterKey length] > 0) + reporterKey = crashReporterKey; + + NSString *hardwareModel = @"???"; + if (report.hasMachineInfo && report.machineInfo.modelName != nil) + hardwareModel = report.machineInfo.modelName; + + NSString *incidentIdentifier = @"???"; + if (report.uuidRef != NULL) { + incidentIdentifier = (NSString *) CFBridgingRelease(CFUUIDCreateString(NULL, report.uuidRef)); + } + + [text appendFormat: @"Incident Identifier: %@\n", incidentIdentifier]; + [text appendFormat: @"CrashReporter Key: %@\n", reporterKey]; + [text appendFormat: @"Hardware Model: %@\n", hardwareModel]; + } + + /* Application and process info */ + { + NSString *unknownString = @"???"; + + NSString *processName = unknownString; + NSString *processId = unknownString; + NSString *processPath = unknownString; + NSString *parentProcessName = unknownString; + NSString *parentProcessId = unknownString; + + /* Process information was not available in earlier crash report versions */ + if (report.hasProcessInfo) { + /* Process Name */ + if (report.processInfo.processName != nil) + processName = report.processInfo.processName; + + /* PID */ + processId = [@(report.processInfo.processID) stringValue]; + + /* Process Path */ + if (report.processInfo.processPath != nil) { + processPath = report.processInfo.processPath; + + /* Remove username from the path */ +#if TARGET_OS_SIMULATOR + processPath = [self anonymizedPathFromPath:processPath]; +#endif + } + + /* Parent Process Name */ + if (report.processInfo.parentProcessName != nil) + parentProcessName = report.processInfo.parentProcessName; + + /* Parent Process ID */ + parentProcessId = [@(report.processInfo.parentProcessID) stringValue]; + } + + [text appendFormat: @"Process: %@ [%@]\n", processName, processId]; + [text appendFormat: @"Path: %@\n", processPath]; + [text appendFormat: @"Identifier: %@\n", report.applicationInfo.applicationIdentifier]; + + NSString *marketingVersion = report.applicationInfo.applicationMarketingVersion; + NSString *appVersion = report.applicationInfo.applicationVersion; + NSString *versionString = marketingVersion ? [NSString stringWithFormat:@"%@ (%@)", marketingVersion, appVersion] : appVersion; + + [text appendFormat: @"Version: %@\n", versionString]; + [text appendFormat: @"Code Type: %@\n", codeType]; + [text appendFormat: @"Parent Process: %@ [%@]\n", parentProcessName, parentProcessId]; + } + + [text appendString: @"\n"]; + + NSString *xamarinTrace; + NSString *exceptionReason; + + /* System info */ + { + NSString *osBuild = @"???"; + if (report.systemInfo.operatingSystemBuild != nil) + osBuild = report.systemInfo.operatingSystemBuild; + + NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + NSDateFormatter *rfc3339Formatter = [[NSDateFormatter alloc] init]; + [rfc3339Formatter setLocale:enUSPOSIXLocale]; + [rfc3339Formatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"]; + [rfc3339Formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + + [text appendFormat: @"Date/Time: %@\n", [rfc3339Formatter stringFromDate:report.systemInfo.timestamp]]; + if ([report.processInfo respondsToSelector:@selector(processStartTime)]) { + if (report.systemInfo.timestamp && report.processInfo.processStartTime) { + [text appendFormat: @"Launch Time: %@\n", [rfc3339Formatter stringFromDate:report.processInfo.processStartTime]]; + } + } + [text appendFormat: @"OS Version: %@ %@ (%@)\n", osName, report.systemInfo.operatingSystemVersion, osBuild]; + + // Check if exception data contains xamarin stacktrace in order to determine report version + if (report.hasExceptionInfo) { + exceptionReason = report.exceptionInfo.exceptionReason; + NSInteger xamarinTracePosition = [exceptionReason rangeOfString:BITXamarinStackTraceDelimiter].location; + if (xamarinTracePosition != NSNotFound) { + xamarinTrace = [exceptionReason substringFromIndex:xamarinTracePosition]; + xamarinTrace = [xamarinTrace stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + xamarinTrace = [xamarinTrace stringByReplacingOccurrencesOfString:@"<---\n\n--->" withString:@"<---\n--->"]; + exceptionReason = [exceptionReason substringToIndex:xamarinTracePosition]; + exceptionReason = [exceptionReason stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + } + } + NSString *reportVersion = (xamarinTrace) ? @"104-Xamarin" : @"104"; + [text appendFormat: @"Report Version: %@\n", reportVersion]; + } + + [text appendString: @"\n"]; + + /* Exception code */ + [text appendFormat: @"Exception Type: %@\n", report.signalInfo.name]; + [text appendFormat: @"Exception Codes: %@ at 0x%" PRIx64 "\n", report.signalInfo.code, report.signalInfo.address]; + + for (BITPLCrashReportThreadInfo *thread in report.threads) { + if (thread.crashed) { + [text appendFormat: @"Crashed Thread: %ld\n", (long) thread.threadNumber]; + break; + } + } + + [text appendString: @"\n"]; + + BITPLCrashReportThreadInfo *crashed_thread = nil; + for (BITPLCrashReportThreadInfo *thread in report.threads) { + if (thread.crashed) { + crashed_thread = thread; + break; + } + } + + /* Uncaught Exception */ + if (report.hasExceptionInfo) { + [text appendFormat: @"Application Specific Information:\n"]; + [text appendFormat: @"*** Terminating app due to uncaught exception '%@', reason: '%@'\n", + report.exceptionInfo.exceptionName, exceptionReason]; + [text appendString: @"\n"]; + + /* Xamarin Exception */ + if (xamarinTrace) { + [text appendFormat:@"%@\n", xamarinTrace]; + [text appendString: @"\n"]; + } + + } else if (crashed_thread != nil) { + // try to find the selector in case this was a crash in obj_msgSend + // we search this whether the crash happened in obj_msgSend or not since we don't have the symbol! + + NSString *foundSelector = nil; + + // search the registers value for the current arch +#if TARGET_OS_SIMULATOR + if (lp64) { + foundSelector = [[self class] selectorForRegisterWithName:@"rsi" ofThread:crashed_thread report:report]; + if (foundSelector == NULL) + foundSelector = [[self class] selectorForRegisterWithName:@"rdx" ofThread:crashed_thread report:report]; + } else { + foundSelector = [[self class] selectorForRegisterWithName:@"ecx" ofThread:crashed_thread report:report]; + } +#else + if (lp64) { + foundSelector = [[self class] selectorForRegisterWithName:@"x1" ofThread:crashed_thread report:report]; + } else { + foundSelector = [[self class] selectorForRegisterWithName:@"r1" ofThread:crashed_thread report:report]; + if (foundSelector == NULL) + foundSelector = [[self class] selectorForRegisterWithName:@"r2" ofThread:crashed_thread report:report]; + } +#endif + + if (foundSelector) { + [text appendFormat: @"Application Specific Information:\n"]; + [text appendFormat: @"Selector name found in current argument registers: %@\n", foundSelector]; + [text appendString: @"\n"]; + } + } + + /* If an exception stack trace is available, output an Apple-compatible backtrace. */ + if (report.exceptionInfo != nil && report.exceptionInfo.stackFrames != nil && [report.exceptionInfo.stackFrames count] > 0) { + BITPLCrashReportExceptionInfo *exception = report.exceptionInfo; + + /* Create the header. */ + [text appendString: @"Last Exception Backtrace:\n"]; + + /* Write out the frames. In raw reports, Apple writes this out as a simple list of PCs. In the minimally + * post-processed report, Apple writes this out as full frame entries. We use the latter format. */ + for (NSUInteger frame_idx = 0; frame_idx < [exception.stackFrames count]; frame_idx++) { + BITPLCrashReportStackFrameInfo *frameInfo = exception.stackFrames[frame_idx]; + [text appendString: [[self class] bit_formatStackFrame: frameInfo frameIndex: frame_idx report: report lp64: lp64]]; + } + [text appendString: @"\n"]; + } + + /* Threads */ + NSInteger maxThreadNum = 0; + for (BITPLCrashReportThreadInfo *thread in report.threads) { + if (thread.crashed) { + [text appendFormat: @"Thread %ld Crashed:\n", (long) thread.threadNumber]; + } else { + [text appendFormat: @"Thread %ld:\n", (long) thread.threadNumber]; + } + for (NSUInteger frame_idx = 0; frame_idx < [thread.stackFrames count]; frame_idx++) { + BITPLCrashReportStackFrameInfo *frameInfo = thread.stackFrames[frame_idx]; + [text appendString:[[self class] bit_formatStackFrame:frameInfo frameIndex:frame_idx report:report lp64:lp64]]; + } + [text appendString: @"\n"]; + + /* Track the highest thread number */ + maxThreadNum = MAX(maxThreadNum, thread.threadNumber); + } + + /* Registers */ + if (crashed_thread != nil) { + [text appendFormat: @"Thread %ld crashed with %@ Thread State:\n", (long) crashed_thread.threadNumber, codeType]; + + int regColumn = 0; + for (BITPLCrashReportRegisterInfo *reg in crashed_thread.registers) { + NSString *reg_fmt; + + /* Use 32-bit or 64-bit fixed width format for the register values */ + if (lp64) + reg_fmt = @"%6s: 0x%016" PRIx64 " "; + else + reg_fmt = @"%6s: 0x%08" PRIx64 " "; + + /* Remap register names to match Apple's crash reports */ + NSString *regName = reg.registerName; + if (report.machineInfo != nil && report.machineInfo.processorInfo.typeEncoding == PLCrashReportProcessorTypeEncodingMach) { + BITPLCrashReportProcessorInfo *pinfo = report.machineInfo.processorInfo; + cpu_type_t arch_type = (cpu_type_t)(pinfo.type & ~CPU_ARCH_MASK); + + /* Apple uses 'ip' rather than 'r12' on ARM */ + if (arch_type == CPU_TYPE_ARM && [regName isEqual: @"r12"]) { + regName = @"ip"; + } + } + [text appendFormat: reg_fmt, [regName UTF8String], reg.registerValue]; + + regColumn++; + if (regColumn == 4) { + [text appendString: @"\n"]; + regColumn = 0; + } + } + + if (regColumn != 0) + [text appendString: @"\n"]; + + [text appendString: @"\n"]; + } + + /* Images. The iPhone crash report format sorts these in ascending order, by the base address */ + [text appendString: @"Binary Images:\n"]; + NSMutableArray *addedImagesBaseAddresses = @[].mutableCopy; + for (BITPLCrashReportBinaryImageInfo *imageInfo in [report.images sortedArrayUsingFunction: bit_binaryImageSort context: nil]) { + // Make sure we don't add duplicates + if ([addedImagesBaseAddresses containsObject:@(imageInfo.imageBaseAddress)]) { + continue; + } else { + [addedImagesBaseAddresses addObject:@(imageInfo.imageBaseAddress)]; + } + + NSString *uuid; + /* Fetch the UUID if it exists */ + if (imageInfo.hasImageUUID) + uuid = imageInfo.imageUUID; + else + uuid = @"???"; + + /* Determine the architecture string */ + NSString *archName = [[self class] bit_archNameFromImageInfo:imageInfo]; + + /* Determine if this is the main executable or an app specific framework*/ + NSString *binaryDesignator = @" "; + BITBinaryImageType imageType = [[self class] bit_imageTypeForImagePath:imageInfo.imageName + processPath:report.processInfo.processPath]; + if (imageType != BITBinaryImageTypeOther) { + binaryDesignator = @"+"; + } + + /* base_address - terminating_address [designator]file_name arch file_path */ + NSString *fmt = nil; + if (lp64) { + fmt = @"%18#" PRIx64 " - %18#" PRIx64 " %@%@ %@ <%@> %@\n"; + } else { + fmt = @"%10#" PRIx64 " - %10#" PRIx64 " %@%@ %@ <%@> %@\n"; + } + + /* Remove username from the image path */ + NSString *imageName = @""; + if (imageInfo.imageName && [imageInfo.imageName length] > 0) { +#if TARGET_OS_SIMULATOR + imageName = [imageInfo.imageName stringByAbbreviatingWithTildeInPath]; +#else + imageName = imageInfo.imageName; +#endif + } +#if TARGET_OS_SIMULATOR + imageName = [self anonymizedPathFromPath:imageName]; +#endif + [text appendFormat: fmt, + imageInfo.imageBaseAddress, + imageInfo.imageBaseAddress + (MAX(1U, imageInfo.imageSize) - 1), // The Apple format uses an inclusive range + binaryDesignator, + [imageInfo.imageName lastPathComponent], + archName, + uuid, + imageName]; + } + + + return text; +} + +/** + * Return the selector string of a given register name + * + * @param regName The name of the register to use for getting the address + * @param thread The crashed thread + * @param report The crash report. + * + * @return The selector as a C string or NULL if no selector was found + */ ++ (NSString *)selectorForRegisterWithName:(NSString *)regName ofThread:(BITPLCrashReportThreadInfo *)thread report:(BITPLCrashReport *)report { + // get the address for the register + uint64_t regAddress = 0; + + for (BITPLCrashReportRegisterInfo *reg in thread.registers) { + if ([reg.registerName isEqualToString:regName]) { + regAddress = reg.registerValue; + break; + } + } + + if (regAddress == 0) + return nil; + + BITPLCrashReportBinaryImageInfo *imageForRegAddress = [report imageForAddress:regAddress]; + if (imageForRegAddress) { + // get the SEL + const char *foundSelector = findSEL([imageForRegAddress.imageName UTF8String], imageForRegAddress.imageUUID, regAddress - (uint64_t)imageForRegAddress.imageBaseAddress); + + if (foundSelector != NULL) { + return [NSString stringWithUTF8String:foundSelector]; + } + } + + return nil; +} + + +/** + * Returns an array of app UUIDs and their architecture + * As a dictionary for each element + * + * @param report The report to format. + * + * @return Returns the formatted result on success, or nil if an error occurs. + */ ++ (NSArray *)arrayOfAppUUIDsForCrashReport:(BITPLCrashReport *)report { + NSMutableArray* appUUIDs = [NSMutableArray array]; + + /* Images. The iPhone crash report format sorts these in ascending order, by the base address */ + for (BITPLCrashReportBinaryImageInfo *imageInfo in [report.images sortedArrayUsingFunction: bit_binaryImageSort context: nil]) { + NSString *uuid; + /* Fetch the UUID if it exists */ + uuid = imageInfo.hasImageUUID ? imageInfo.imageUUID : @"???"; + + /* Determine the architecture string */ + NSString *archName = [[self class] bit_archNameFromImageInfo:imageInfo]; + + /* Determine if this is the app executable or app specific framework */ + BITBinaryImageType imageType = [[self class] bit_imageTypeForImagePath:imageInfo.imageName + processPath:report.processInfo.processPath]; + if (imageType != BITBinaryImageTypeOther) { + NSString *imageTypeString; + + if (imageType == BITBinaryImageTypeAppBinary) { + imageTypeString = @"app"; + } else { + imageTypeString = @"framework"; + } + + [appUUIDs addObject:@{kBITBinaryImageKeyUUID: uuid, + kBITBinaryImageKeyArch: archName, + kBITBinaryImageKeyType: imageTypeString} + ]; + } + } + + return appUUIDs; +} + +/* Determine if in binary image is the app executable or app specific framework */ ++ (BITBinaryImageType)bit_imageTypeForImagePath:(NSString *)imagePath processPath:(NSString *)processPath { + if (!imagePath || !processPath) { + return BITBinaryImageTypeOther; + } + BITBinaryImageType imageType = BITBinaryImageTypeOther; + + NSString *standardizedImagePath = [[imagePath stringByStandardizingPath] lowercaseString]; + NSString *lowercaseImagePath = [imagePath lowercaseString]; + NSString *lowercaseProcessPath = [processPath lowercaseString]; + + NSRange appRange = [standardizedImagePath rangeOfString: @".app/"]; + + // Exclude iOS swift dylibs. These are provided as part of the app binary by Xcode for now, but we never get a dSYM for those. + NSRange swiftLibRange = [standardizedImagePath rangeOfString:@"frameworks/libswift"]; + BOOL dylibSuffix = [standardizedImagePath hasSuffix:@".dylib"]; + + if (appRange.location != NSNotFound && !(swiftLibRange.location != NSNotFound && dylibSuffix)) { + NSString *appBundleContentsPath = [standardizedImagePath substringToIndex:appRange.location + 5]; + + if ([standardizedImagePath isEqual: lowercaseProcessPath] || + // Fix issue with iOS 8 `stringByStandardizingPath` removing leading `/private` path (when not running in the debugger or simulator only) + [lowercaseImagePath hasPrefix:lowercaseProcessPath]) { + imageType = BITBinaryImageTypeAppBinary; + } else if ([standardizedImagePath hasPrefix:appBundleContentsPath] || + // Fix issue with iOS 8 `stringByStandardizingPath` removing leading `/private` path (when not running in the debugger or simulator only) + [lowercaseImagePath hasPrefix:appBundleContentsPath]) { + imageType = BITBinaryImageTypeAppFramework; + } + } + + return imageType; +} + ++ (NSString *)bit_archNameFromImageInfo:(BITPLCrashReportBinaryImageInfo *)imageInfo +{ + NSString *archName = @"???"; + if (imageInfo.codeType != nil && imageInfo.codeType.typeEncoding == PLCrashReportProcessorTypeEncodingMach) { + archName = [BITCrashReportTextFormatter bit_archNameFromCPUType:imageInfo.codeType.type subType:imageInfo.codeType.subtype]; + } + + return archName; +} + ++ (NSString *)bit_archNameFromCPUType:(uint64_t)cpuType subType:(uint64_t)subType { + NSString *archName = @"???"; + switch (cpuType) { + case CPU_TYPE_ARM: + /* Apple includes subtype for ARM binaries. */ + switch (subType) { + case CPU_SUBTYPE_ARM_V6: + archName = @"armv6"; + break; + + case CPU_SUBTYPE_ARM_V7: + archName = @"armv7"; + break; + + case CPU_SUBTYPE_ARM_V7S: + archName = @"armv7s"; + break; + + default: + archName = @"arm-unknown"; + break; + } + break; + + case CPU_TYPE_ARM64: + /* Apple includes subtype for ARM64 binaries. */ + switch (subType) { + case CPU_SUBTYPE_ARM_ALL: + archName = @"arm64"; + break; + + case CPU_SUBTYPE_ARM_V8: + archName = @"arm64"; + break; + + default: + archName = @"arm64-unknown"; + break; + } + break; + + case CPU_TYPE_X86: + archName = @"i386"; + break; + + case CPU_TYPE_X86_64: + archName = @"x86_64"; + break; + + case CPU_TYPE_POWERPC: + archName = @"powerpc"; + break; + + default: + // Use the default archName value (initialized above). + break; + } + + return archName; +} + + +/** + * Format a stack frame for display in a thread backtrace. + * + * @param frameInfo The stack frame to format + * @param frameIndex The frame's index + * @param report The report from which this frame was acquired. + * @param lp64 If YES, the report was generated by an LP64 system. + * + * @return Returns a formatted frame line. + */ ++ (NSString *)bit_formatStackFrame: (BITPLCrashReportStackFrameInfo *) frameInfo + frameIndex: (NSUInteger) frameIndex + report: (BITPLCrashReport *) report + lp64: (boolean_t) lp64 +{ + /* Base image address containing instrumentation pointer, offset of the IP from that base + * address, and the associated image name */ + uint64_t baseAddress = 0x0; + uint64_t pcOffset = 0x0; + NSString *imageName = @"\?\?\?"; + NSString *symbolString = nil; + + BITPLCrashReportBinaryImageInfo *imageInfo = [report imageForAddress: frameInfo.instructionPointer]; + if (imageInfo != nil) { + imageName = [imageInfo.imageName lastPathComponent]; + baseAddress = imageInfo.imageBaseAddress; + pcOffset = frameInfo.instructionPointer - imageInfo.imageBaseAddress; + } + + /* Make sure UTF8/16 characters are handled correctly */ + NSInteger offset = 0; + NSUInteger index = 0; + for (index = 0; index < [imageName length]; index++) { + NSRange range = [imageName rangeOfComposedCharacterSequenceAtIndex:index]; + if (range.length > 1) { + offset += range.length - 1; + index += range.length - 1; + } + if (index > 32) { + imageName = [NSString stringWithFormat:@"%@... ", [imageName substringToIndex:index - 1]]; + index += 3; + break; + } + } + if (index-offset < 36) { + imageName = [imageName stringByPaddingToLength:(NSUInteger)(36 + offset) withString:@" " startingAtIndex:0]; + } + + /* If symbol info is available, the format used in Apple's reports is Sym + OffsetFromSym. Otherwise, + * the format used is imageBaseAddress + offsetToIP */ + BITBinaryImageType imageType = [[self class] bit_imageTypeForImagePath:imageInfo.imageName + processPath:report.processInfo.processPath]; + if (frameInfo.symbolInfo != nil && imageType == BITBinaryImageTypeOther) { + NSString *symbolName = frameInfo.symbolInfo.symbolName; + + /* Apple strips the _ symbol prefix in their reports. Only OS X makes use of an + * underscore symbol prefix by default. */ + if ([symbolName rangeOfString: @"_"].location == 0 && [symbolName length] > 1) { + switch (report.systemInfo.operatingSystem) { + case PLCrashReportOperatingSystemMacOSX: + case PLCrashReportOperatingSystemiPhoneOS: + case PLCrashReportOperatingSystemiPhoneSimulator: + symbolName = [symbolName substringFromIndex: 1]; + break; + + default: + BITHockeyLogDebug(@"Symbol prefix rules are unknown for this OS!"); + break; + } + } + + + uint64_t symOffset = frameInfo.instructionPointer - frameInfo.symbolInfo.startAddress; + symbolString = [NSString stringWithFormat: @"%@ + %" PRId64, symbolName, symOffset]; + } else { + symbolString = [NSString stringWithFormat: @"0x%" PRIx64 " + %" PRId64, baseAddress, pcOffset]; + } + + /* Note that width specifiers are ignored for %@, but work for C strings. + * UTF-8 is not correctly handled with %s (it depends on the system encoding), but + * UTF-16 is supported via %S, so we use it here */ + return [NSString stringWithFormat: @"%-4ld%-35S 0x%0*" PRIx64 " %@\n", + (long) frameIndex, + (const uint16_t *)[imageName cStringUsingEncoding: NSUTF16StringEncoding], + lp64 ? 16 : 8, frameInfo.instructionPointer, + symbolString]; +} + +/** + * Remove the user's name from a crash's process path. + * This is only necessary when sending crashes from the simulator as the path + * then contains the username of the Mac the simulator is running on. + * + * @param path A string containing the username. + * + * @return An anonymized string where the real username is replaced by "USER" + */ ++ (NSString *)anonymizedPathFromPath:(NSString *)path { + + NSString *anonymizedProcessPath = [NSString string]; + + if (([path length] > 0) && [path hasPrefix:@"/Users/"]) { + NSError *error = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(/Users/[^/]+/)" options:0 error:&error]; + anonymizedProcessPath = [regex stringByReplacingMatchesInString:path options:0 range:NSMakeRange(0, [path length]) withTemplate:@"/Users/USER/"]; + if (error) { + BITHockeyLogError(@"ERROR: String replacing failed - %@", error.localizedDescription); + } + } + else if(([path length] > 0) && (![path containsString:@"Users"])) { + return path; + } + return anonymizedProcessPath; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ diff --git a/submodules/HockeySDK-iOS/Classes/BITCrashReportTextFormatterPrivate.h b/submodules/HockeySDK-iOS/Classes/BITCrashReportTextFormatterPrivate.h new file mode 100644 index 0000000000..5b53c4f231 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITCrashReportTextFormatterPrivate.h @@ -0,0 +1,20 @@ +// +// BITCrashReportTextFormatterPrivate.h +// HockeySDK +// +// Created by Lukas Spieß on 27/01/16. +// +// + +#import "BITCrashReportTextFormatter.h" + +#ifndef BITCrashReportTextFormatterPrivate_h +#define BITCrashReportTextFormatterPrivate_h + +@interface BITCrashReportTextFormatter () + ++ (NSString *)anonymizedPathFromPath:(NSString *)path; + +@end + +#endif /* BITCrashReportTextFormatterPrivate_h */ diff --git a/submodules/HockeySDK-iOS/Classes/BITData.h b/submodules/HockeySDK-iOS/Classes/BITData.h new file mode 100644 index 0000000000..05147d6b95 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITData.h @@ -0,0 +1,13 @@ +#import "BITBase.h" +@class BITTelemetryData; + +@interface BITData : BITBase + +@property (nonatomic, strong) BITTelemetryData *baseData; + +- (instancetype)initWithCoder:(NSCoder *)coder; + +- (void)encodeWithCoder:(NSCoder *)coder; + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITData.m b/submodules/HockeySDK-iOS/Classes/BITData.m new file mode 100644 index 0000000000..958e6ac9aa --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITData.m @@ -0,0 +1,39 @@ +#import "BITData.h" +#import "BITHockeyLogger.h" + +/// Data contract class for type Data. +@implementation BITData + +/// +/// Adds all members of this class to a dictionary +/// @returns dictionary to which the members of this class will be added. +/// +- (NSDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary].mutableCopy; + NSDictionary *baseDataDict = [self.baseData serializeToDictionary]; + if ([NSJSONSerialization isValidJSONObject:baseDataDict]) { + [dict setObject:baseDataDict forKey:@"baseData"]; + } else { + BITHockeyLogError(@"[HockeySDK] Some of the telemetry data was not NSJSONSerialization compatible and could not be serialized!"); + } + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if(self) { + _baseData = [coder decodeObjectForKey:@"self.baseData"]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.baseData forKey:@"self.baseData"]; +} + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITDevice.h b/submodules/HockeySDK-iOS/Classes/BITDevice.h new file mode 100755 index 0000000000..95fa4d52af --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITDevice.h @@ -0,0 +1,27 @@ +#import "BITTelemetryObject.h" + +@interface BITDevice : BITTelemetryObject + +@property (nonatomic, copy) NSString *deviceId; +@property (nonatomic, copy) NSString *ip; +@property (nonatomic, copy) NSString *language; +@property (nonatomic, copy) NSString *locale; +@property (nonatomic, copy) NSString *machineName; +@property (nonatomic, copy) NSString *model; +@property (nonatomic, copy) NSString *network; +@property (nonatomic, copy) NSString *networkName; +@property (nonatomic, copy) NSString *oemName; +@property (nonatomic, copy) NSString *os; +@property (nonatomic, copy) NSString *osVersion; +@property (nonatomic, copy) NSString *roleInstance; +@property (nonatomic, copy) NSString *roleName; +@property (nonatomic, copy) NSString *screenResolution; +@property (nonatomic, copy) NSString *type; +@property (nonatomic, copy) NSString *vmName; + +- (instancetype)initWithCoder:(NSCoder *)coder; + +- (void)encodeWithCoder:(NSCoder *)coder; + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITDevice.m b/submodules/HockeySDK-iOS/Classes/BITDevice.m new file mode 100755 index 0000000000..54eb987a74 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITDevice.m @@ -0,0 +1,110 @@ +#import "BITDevice.h" + +/// Data contract class for type Device. +@implementation BITDevice + +/// +/// Adds all members of this class to a dictionary +/// @returns dictionary to which the members of this class will be added. +/// +- (NSDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary].mutableCopy; + if (self.deviceId != nil) { + [dict setObject:self.deviceId forKey:@"ai.device.id"]; + } + if (self.ip != nil) { + [dict setObject:self.ip forKey:@"ai.device.ip"]; + } + if (self.language != nil) { + [dict setObject:self.language forKey:@"ai.device.language"]; + } + if (self.locale != nil) { + [dict setObject:self.locale forKey:@"ai.device.locale"]; + } + if (self.model != nil) { + [dict setObject:self.model forKey:@"ai.device.model"]; + } + if (self.network != nil) { + [dict setObject:self.network forKey:@"ai.device.network"]; + } + if(self.networkName != nil) { + [dict setObject:self.networkName forKey:@"ai.device.networkName"]; + } + if (self.oemName != nil) { + [dict setObject:self.oemName forKey:@"ai.device.oemName"]; + } + if (self.os != nil) { + [dict setObject:self.os forKey:@"ai.device.os"]; + } + if (self.osVersion != nil) { + [dict setObject:self.osVersion forKey:@"ai.device.osVersion"]; + } + if (self.roleInstance != nil) { + [dict setObject:self.roleInstance forKey:@"ai.device.roleInstance"]; + } + if (self.roleName != nil) { + [dict setObject:self.roleName forKey:@"ai.device.roleName"]; + } + if (self.screenResolution != nil) { + [dict setObject:self.screenResolution forKey:@"ai.device.screenResolution"]; + } + if (self.type != nil) { + [dict setObject:self.type forKey:@"ai.device.type"]; + } + if (self.machineName != nil) { + [dict setObject:self.machineName forKey:@"ai.device.machineName"]; + } + if(self.vmName != nil) { + [dict setObject:self.vmName forKey:@"ai.device.vmName"]; + } + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if(self) { + _deviceId = [coder decodeObjectForKey:@"self.deviceId"]; + _ip = [coder decodeObjectForKey:@"self.ip"]; + _language = [coder decodeObjectForKey:@"self.language"]; + _locale = [coder decodeObjectForKey:@"self.locale"]; + _model = [coder decodeObjectForKey:@"self.model"]; + _network = [coder decodeObjectForKey:@"self.network"]; + _oemName = [coder decodeObjectForKey:@"self.oemName"]; + _os = [coder decodeObjectForKey:@"self.os"]; + _osVersion = [coder decodeObjectForKey:@"self.osVersion"]; + _roleInstance = [coder decodeObjectForKey:@"self.roleInstance"]; + _roleName = [coder decodeObjectForKey:@"self.roleName"]; + _screenResolution = [coder decodeObjectForKey:@"self.screenResolution"]; + _type = [coder decodeObjectForKey:@"self.type"]; + _machineName = [coder decodeObjectForKey:@"self.machineName"]; + _networkName = [coder decodeObjectForKey:@"self.networkName"]; + _vmName = [coder decodeObjectForKey:@"self.vmName"]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.deviceId forKey:@"self.deviceId"]; + [coder encodeObject:self.ip forKey:@"self.ip"]; + [coder encodeObject:self.language forKey:@"self.language"]; + [coder encodeObject:self.locale forKey:@"self.locale"]; + [coder encodeObject:self.model forKey:@"self.model"]; + [coder encodeObject:self.network forKey:@"self.network"]; + [coder encodeObject:self.networkName forKey:@"self.networkName"]; + [coder encodeObject:self.oemName forKey:@"self.oemName"]; + [coder encodeObject:self.os forKey:@"self.os"]; + [coder encodeObject:self.osVersion forKey:@"self.osVersion"]; + [coder encodeObject:self.roleInstance forKey:@"self.roleInstance"]; + [coder encodeObject:self.roleName forKey:@"self.roleName"]; + [coder encodeObject:self.screenResolution forKey:@"self.screenResolution"]; + [coder encodeObject:self.type forKey:@"self.type"]; + [coder encodeObject:self.machineName forKey:@"self.machineName"]; + [coder encodeObject:self.vmName forKey:@"self.vmName"]; +} + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITDomain.h b/submodules/HockeySDK-iOS/Classes/BITDomain.h new file mode 100644 index 0000000000..c20cd5a71f --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITDomain.h @@ -0,0 +1,5 @@ +#import "BITTelemetryData.h" + +@interface BITDomain : BITTelemetryData + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITDomain.m b/submodules/HockeySDK-iOS/Classes/BITDomain.m new file mode 100644 index 0000000000..d1d743af04 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITDomain.m @@ -0,0 +1,45 @@ +#import "BITDomain.h" +/// Data contract class for type Domain. +@implementation BITDomain +@synthesize envelopeTypeName = _envelopeTypeName; +@synthesize dataTypeName = _dataTypeName; +@synthesize properties = _properties; + +/// Initializes a new instance of the class. +- (instancetype)init { + if ((self = [super init])) { + _envelopeTypeName = @"Microsoft.ApplicationInsights.Domain"; + _dataTypeName = @"Domain"; + } + return self; +} + +/// +/// Adds all members of this class to a dictionary +/// @returns dictionary to which the members of this class will be added. +/// +- (NSDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary].mutableCopy; + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if(self) { + _envelopeTypeName = (NSString *)[coder decodeObjectForKey:@"_envelopeTypeName"]; + _dataTypeName = (NSString *)[coder decodeObjectForKey:@"_dataTypeName"]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.envelopeTypeName forKey:@"_envelopeTypeName"]; + [coder encodeObject:self.dataTypeName forKey:@"_dataTypeName"]; +} + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITEnvelope.h b/submodules/HockeySDK-iOS/Classes/BITEnvelope.h new file mode 100644 index 0000000000..5237132215 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITEnvelope.h @@ -0,0 +1,23 @@ +#import "BITTelemetryObject.h" +@class BITBase; + +@interface BITEnvelope : BITTelemetryObject + +@property (nonatomic, copy) NSNumber *version; +@property (nonatomic, copy) NSString *name; +@property (nonatomic, copy) NSString *time; +@property (nonatomic, copy) NSNumber *sampleRate; +@property (nonatomic, copy) NSString *seq; +@property (nonatomic, copy) NSString *iKey; +@property (nonatomic, copy) NSNumber *flags; +@property (nonatomic, copy) NSString *deviceId; +@property (nonatomic, copy) NSString *os; +@property (nonatomic, copy) NSString *osVer; +@property (nonatomic, copy) NSString *appId; +@property (nonatomic, copy) NSString *appVer; +@property (nonatomic, copy) NSString *userId; +@property (nonatomic, strong) NSDictionary *tags; +@property (nonatomic, strong) BITBase *data; + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITEnvelope.m b/submodules/HockeySDK-iOS/Classes/BITEnvelope.m new file mode 100644 index 0000000000..b833f58f89 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITEnvelope.m @@ -0,0 +1,120 @@ +#import "BITEnvelope.h" +#import "BITData.h" +#import "BITHockeyLogger.h" + +/// Data contract class for type Envelope. +@implementation BITEnvelope + +/// Initializes a new instance of the class. +- (instancetype)init { + if((self = [super init])) { + _version = @1; + _sampleRate = @100.0; + _tags = [NSDictionary dictionary]; + } + return self; +} + +/// +/// Adds all members of this class to a dictionary +/// @returns dictionary to which the members of this class will be added. +/// +- (NSDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary].mutableCopy; + if(self.version != nil) { + [dict setObject:self.version forKey:@"ver"]; + } + if(self.name != nil) { + [dict setObject:self.name forKey:@"name"]; + } + if(self.time != nil) { + [dict setObject:self.time forKey:@"time"]; + } + if(self.sampleRate != nil) { + [dict setObject:self.sampleRate forKey:@"sampleRate"]; + } + if(self.seq != nil) { + [dict setObject:self.seq forKey:@"seq"]; + } + if(self.iKey != nil) { + [dict setObject:self.iKey forKey:@"iKey"]; + } + if(self.flags != nil) { + [dict setObject:self.flags forKey:@"flags"]; + } + if(self.deviceId != nil) { + [dict setObject:self.deviceId forKey:@"deviceId"]; + } + if(self.os != nil) { + [dict setObject:self.os forKey:@"os"]; + } + if(self.osVer != nil) { + [dict setObject:self.osVer forKey:@"osVer"]; + } + if(self.appId != nil) { + [dict setObject:self.appId forKey:@"appId"]; + } + if(self.appVer != nil) { + [dict setObject:self.appVer forKey:@"appVer"]; + } + if(self.userId != nil) { + [dict setObject:self.userId forKey:@"userId"]; + } + if(self.tags != nil) { + [dict setObject:self.tags forKey:@"tags"]; + } + + NSDictionary *dataDict = [self.data serializeToDictionary]; + if ([NSJSONSerialization isValidJSONObject:dataDict]) { + [dict setObject:dataDict forKey:@"data"]; + } else { + BITHockeyLogError(@"[HockeySDK] Some of the telemetry data was not NSJSONSerialization compatible and could not be serialized!"); + } + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if(self) { + _version = [coder decodeObjectForKey:@"self.version"]; + _name = [coder decodeObjectForKey:@"self.name"]; + _time = [coder decodeObjectForKey:@"self.time"]; + _sampleRate = [coder decodeObjectForKey:@"self.sampleRate"]; + _seq = [coder decodeObjectForKey:@"self.seq"]; + _iKey = [coder decodeObjectForKey:@"self.iKey"]; + _flags = [coder decodeObjectForKey:@"self.flags"]; + _deviceId = [coder decodeObjectForKey:@"self.deviceId"]; + _os = [coder decodeObjectForKey:@"self.os"]; + _osVer = [coder decodeObjectForKey:@"self.osVer"]; + _appId = [coder decodeObjectForKey:@"self.appId"]; + _appVer = [coder decodeObjectForKey:@"self.appVer"]; + _userId = [coder decodeObjectForKey:@"self.userId"]; + _tags = [coder decodeObjectForKey:@"self.tags"]; + _data = [coder decodeObjectForKey:@"self.data"]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.version forKey:@"self.version"]; + [coder encodeObject:self.name forKey:@"self.name"]; + [coder encodeObject:self.time forKey:@"self.time"]; + [coder encodeObject:self.sampleRate forKey:@"self.sampleRate"]; + [coder encodeObject:self.seq forKey:@"self.seq"]; + [coder encodeObject:self.iKey forKey:@"self.iKey"]; + [coder encodeObject:self.flags forKey:@"self.flags"]; + [coder encodeObject:self.deviceId forKey:@"self.deviceId"]; + [coder encodeObject:self.os forKey:@"self.os"]; + [coder encodeObject:self.osVer forKey:@"self.osVer"]; + [coder encodeObject:self.appId forKey:@"self.appId"]; + [coder encodeObject:self.appVer forKey:@"self.appVer"]; + [coder encodeObject:self.userId forKey:@"self.userId"]; + [coder encodeObject:self.tags forKey:@"self.tags"]; + [coder encodeObject:self.data forKey:@"self.data"]; +} + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITEventData.h b/submodules/HockeySDK-iOS/Classes/BITEventData.h new file mode 100644 index 0000000000..50bb133330 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITEventData.h @@ -0,0 +1,9 @@ +#import "BITDomain.h" + +@interface BITEventData : BITDomain + +@property (nonatomic, copy, readonly) NSString *envelopeTypeName; +@property (nonatomic, copy, readonly) NSString *dataTypeName; +@property (nonatomic, strong) NSDictionary *measurements; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITEventData.m b/submodules/HockeySDK-iOS/Classes/BITEventData.m new file mode 100644 index 0000000000..b5ef6f07c9 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITEventData.m @@ -0,0 +1,66 @@ +#import "BITEventData.h" + +/// Data contract class for type EventData. +@implementation BITEventData +@synthesize envelopeTypeName = _envelopeTypeName; +@synthesize dataTypeName = _dataTypeName; +@synthesize version = _version; +@synthesize properties = _properties; +@synthesize measurements = _measurements; + +/// Initializes a new instance of the class. +- (instancetype)init { + if ((self = [super init])) { + _envelopeTypeName = @"Microsoft.ApplicationInsights.Event"; + _dataTypeName = @"EventData"; + _version = @2; + _properties = [NSDictionary new]; + _measurements = [NSDictionary new]; + } + return self; +} + +/// +/// Adds all members of this class to a dictionary +/// @returns dictionary to which the members of this class will be added. +/// +- (NSDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary].mutableCopy; + if (self.name != nil) { + [dict setObject:self.name forKey:@"name"]; + } + if (self.properties !=nil) { + [dict setObject:self.properties forKey:@"properties"]; + } + if (self.measurements) { + [dict setObject:self.measurements forKey:@"measurements"]; + } + + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if(self) { + _envelopeTypeName = [coder decodeObjectForKey:@"self.envelopeTypeName"]; + _dataTypeName = [coder decodeObjectForKey:@"self.dataTypeName"]; + _version = (NSNumber *)[coder decodeObjectForKey:@"self.version"]; + _properties = (NSDictionary *)[coder decodeObjectForKey:@"self.properties"]; + _measurements = [coder decodeObjectForKey:@"self.measurements"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.envelopeTypeName forKey:@"self.envelopeTypeName"]; + [coder encodeObject:self.dataTypeName forKey:@"self.dataTypeName"]; + [coder encodeObject:self.version forKey:@"self.version"]; + [coder encodeObject:self.properties forKey:@"self.properties"]; + [coder encodeObject:self.measurements forKey:@"self.measurements"]; +} + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackActivity.h b/submodules/HockeySDK-iOS/Classes/BITFeedbackActivity.h new file mode 100644 index 0000000000..44c08a985c --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackActivity.h @@ -0,0 +1,75 @@ +/* + * 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 + +#import "BITFeedbackComposeViewControllerDelegate.h" + +/** + UIActivity subclass allowing to use the feedback interface to share content with the developer + + This activity can be added into an UIActivityViewController and it will use the activity data + objects to prefill the content of `BITFeedbackComposeViewController`. + + This can be useful if you present some data that users can not only share but also + report back to the developer because they have some problems, e.g. webcams not working + any more. + + The activity provide a default title and image that can be further customized + via `customActivityImage` and `customActivityTitle`. + + */ + +@interface BITFeedbackActivity : UIActivity + +///----------------------------------------------------------------------------- +/// @name BITFeedbackActivity customisation +///----------------------------------------------------------------------------- + + +/** + Define the image shown when using `BITFeedbackActivity` + + If not set a default icon is being used. + + @see customActivityTitle + */ +@property (nonatomic, strong) UIImage *customActivityImage; + + +/** + Define the title shown when using `BITFeedbackActivity` + + If not set, a default string is shown by using the apps name + and adding the localized string "Feedback" to it. + + @see customActivityImage + */ +@property (nonatomic, copy) NSString *customActivityTitle; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackActivity.m b/submodules/HockeySDK-iOS/Classes/BITFeedbackActivity.m new file mode 100644 index 0000000000..08e1775062 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackActivity.m @@ -0,0 +1,150 @@ +/* + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "HockeySDKPrivate.h" + +#import "BITFeedbackActivity.h" + +#import "BITHockeyHelper.h" +#import "BITFeedbackManagerPrivate.h" + +#import "BITHockeyBaseManagerPrivate.h" +#import "BITHockeyAttachment.h" + + +@interface BITFeedbackActivity() + +@property (nonatomic, strong) NSMutableArray *items; +@property (nonatomic, strong, readwrite) UIViewController *activityViewController; + +@end + + +@implementation BITFeedbackActivity + +@synthesize activityViewController = _activityViewController; + +#pragma mark - NSObject + +- (instancetype)init { + if ((self = [super init])) { + _customActivityImage = nil; + _customActivityTitle = nil; + + _items = [NSMutableArray array]; + } + + return self; +} + + + +#pragma mark - UIActivity + +- (NSString *)activityType { + return @"UIActivityTypePostToHockeySDKFeedback"; +} + +- (NSString *)activityTitle { + if (self.customActivityTitle) + return self.customActivityTitle; + + NSString *appName = bit_appName(BITHockeyLocalizedString(@"HockeyFeedbackActivityAppPlaceholder")); + + return [NSString stringWithFormat:BITHockeyLocalizedString(@"HockeyFeedbackActivityButtonTitle"), appName]; +} + +- (UIImage *)activityImage { + if (self.customActivityImage) + return self.customActivityImage; + + return bit_imageNamed(@"feedbackActivity.png", BITHOCKEYSDK_BUNDLE); +} + + +- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems { + if ([BITHockeyManager sharedHockeyManager].disableFeedbackManager) return NO; + + for (UIActivityItemProvider *item in activityItems) { + if ([item isKindOfClass:[NSString class]]) { + return YES; + } else if ([item isKindOfClass:[UIImage class]]) { + return YES; + } else if ([item isKindOfClass:[NSData class]]) { + return YES; + } else if ([item isKindOfClass:[BITHockeyAttachment class]]) { + return YES; + } else if ([item isKindOfClass:[NSURL class]]) { + return YES; + } + } + return NO; +} + +- (void)prepareWithActivityItems:(NSArray *)activityItems { + for (id item in activityItems) { + if ([item isKindOfClass:[NSString class]] || + [item isKindOfClass:[UIImage class]] || + [item isKindOfClass:[NSData class]] || + [item isKindOfClass:[BITHockeyAttachment class]] || + [item isKindOfClass:[NSURL class]]) { + [self.items addObject:item]; + } else { + BITHockeyLogWarning(@"WARNING: Unknown item type %@", item); + } + } +} + +- (UIViewController *)activityViewController { + if (!_activityViewController) { + // TODO: return compose controller with activity content added + BITFeedbackManager *manager = [BITHockeyManager sharedHockeyManager].feedbackManager; + + BITFeedbackComposeViewController *composeViewController = [manager feedbackComposeViewController]; + composeViewController.delegate = self; + [composeViewController prepareWithItems:self.items]; + + _activityViewController = [manager customNavigationControllerWithRootViewController:composeViewController + presentationStyle:UIModalPresentationFormSheet]; + _activityViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; + } + return _activityViewController; +} + +- (void)feedbackComposeViewController:(BITFeedbackComposeViewController *) __unused composeViewController didFinishWithResult:(BITFeedbackComposeResult)composeResult { + [self activityDidFinish:composeResult == BITFeedbackComposeResultSubmitted]; +} + + +@end + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackComposeViewController.h b/submodules/HockeySDK-iOS/Classes/BITFeedbackComposeViewController.h new file mode 100644 index 0000000000..ec58531894 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackComposeViewController.h @@ -0,0 +1,97 @@ +/* + * 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 + +#import "BITFeedbackComposeViewControllerDelegate.h" + +/** + View controller allowing the user to write and send new feedback + + To add this view controller to your own app and push it onto a navigation stack, + don't create the instance yourself, but use the following code to get a correct instance: + + [[BITHockeyManager sharedHockeyManager].feedbackManager feedbackComposeViewController] + + To show it modally, use the following code instead: + + [[BITHockeyManager sharedHockeyManager].feedbackManager showFeedbackComposeView] + + */ + +@interface BITFeedbackComposeViewController : UIViewController + + +///----------------------------------------------------------------------------- +/// @name Delegate +///----------------------------------------------------------------------------- + + +/** + Sets the `BITFeedbackComposeViewControllerDelegate` delegate. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see [BITHockeyManager setDelegate:] + */ +@property (nonatomic, weak) id delegate; + + +///----------------------------------------------------------------------------- +/// @name Presetting content +///----------------------------------------------------------------------------- + + +/** + Don't show the option to add images from the photo library + + This is helpful if your application is landscape only, since the system UI for + selecting an image from the photo library is portrait only + */ +@property (nonatomic) BOOL hideImageAttachmentButton; + +/** + An array of data objects that should be used to prefill the compose view content + + The following data object classes are currently supported: + - NSString + - NSURL + - UIImage + - NSData + - `BITHockeyAttachment` + + These are automatically concatenated to one text string, while any images and NSData + objects are added as attachments to the feedback. + + @param items Array of data objects to prefill the feedback text message. + */ +- (void)prepareWithItems:(NSArray *)items; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackComposeViewController.m b/submodules/HockeySDK-iOS/Classes/BITFeedbackComposeViewController.m new file mode 100644 index 0000000000..49dfc46699 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackComposeViewController.m @@ -0,0 +1,690 @@ +/* + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "HockeySDKPrivate.h" + +#import "BITFeedbackManagerPrivate.h" +#import "BITFeedbackMessageAttachment.h" +#import "BITFeedbackComposeViewController.h" +#import "BITFeedbackUserDataViewController.h" + +#import "BITHockeyBaseManagerPrivate.h" + +#import "BITHockeyHelper.h" + +#import "BITImageAnnotationViewController.h" +#import "BITHockeyAttachment.h" + +@interface BITFeedbackComposeViewController () { +} + +@property (nonatomic, weak) BITFeedbackManager *manager; +@property (nonatomic, strong) UITextView *textView; +@property (nonatomic, strong) UIView *contentViewContainer; +@property (nonatomic, strong) UIScrollView *attachmentScrollView; +@property (nonatomic, strong) NSMutableArray *attachmentScrollViewImageViews; + +@property (nonatomic, strong) UIButton *addPhotoButton; + +@property (nonatomic, copy) NSString *text; + +@property (nonatomic, strong) NSMutableArray *attachments; +@property (nonatomic, strong) NSMutableArray *imageAttachments; + +@property (nonatomic, strong) UIView *textAccessoryView; +@property (nonatomic) NSInteger selectedAttachmentIndex; +@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; + +@property (nonatomic) BOOL blockUserDataScreen; +@property (nonatomic) BOOL actionSheetVisible; + +/** + * Workaround for UIImagePickerController bug. + * The statusBar shows up when the UIImagePickerController opens. + * The status bar does not disappear again when the UIImagePickerController is dismissed. + * Therefore store the state when UIImagePickerController is shown and restore when viewWillAppear gets called. + */ +@property (nonatomic, strong) NSNumber *isStatusBarHiddenBeforeShowingPhotoPicker; + +@end + + +@implementation BITFeedbackComposeViewController + + +#pragma mark - NSObject + +- (instancetype)init { + self = [super init]; + if (self) { + _blockUserDataScreen = NO; + _actionSheetVisible = NO; + _delegate = nil; + _manager = [BITHockeyManager sharedHockeyManager].feedbackManager; + _attachments = [NSMutableArray new]; + _imageAttachments = [NSMutableArray new]; + _attachmentScrollViewImageViews = [NSMutableArray new]; + _tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollViewTapped:)]; + [_attachmentScrollView addGestureRecognizer:self.tapRecognizer]; + + _text = nil; + } + + return self; +} + + +#pragma mark - Public + +- (void)prepareWithItems:(NSArray *)items { + for (id item in items) { + if ([item isKindOfClass:[NSString class]]) { + self.text = [(self.text ? self.text : @"") stringByAppendingFormat:@"%@%@", (self.text ? @" " : @""), item]; + } else if ([item isKindOfClass:[NSURL class]]) { + self.text = [(self.text ? self.text : @"") stringByAppendingFormat:@"%@%@", (self.text ? @" " : @""), [(NSURL *)item absoluteString]]; + } else if ([item isKindOfClass:[UIImage class]]) { + UIImage *image = item; + BITFeedbackMessageAttachment *attachment = [BITFeedbackMessageAttachment attachmentWithData:UIImageJPEGRepresentation(image, (CGFloat)0.7) contentType:@"image/jpeg"]; + attachment.originalFilename = [NSString stringWithFormat:@"Image_%li.jpg", (unsigned long)[self.attachments count]]; + [self.attachments addObject:attachment]; + [self.imageAttachments addObject:attachment]; + } else if ([item isKindOfClass:[NSData class]]) { + BITFeedbackMessageAttachment *attachment = [BITFeedbackMessageAttachment attachmentWithData:item contentType:@"application/octet-stream"]; + attachment.originalFilename = [NSString stringWithFormat:@"Attachment_%li.data", (unsigned long)[self.attachments count]]; + [self.attachments addObject:attachment]; + } else if ([item isKindOfClass:[BITHockeyAttachment class]]) { + BITHockeyAttachment *sourceAttachment = (BITHockeyAttachment *)item; + + if (!sourceAttachment.hockeyAttachmentData) { + BITHockeyLogDebug(@"BITHockeyAttachment instance doesn't contain any data."); + continue; + } + + NSString *filename = [NSString stringWithFormat:@"Attachment_%li.data", (unsigned long)[self.attachments count]]; + if (sourceAttachment.filename) { + filename = sourceAttachment.filename; + } + + BITFeedbackMessageAttachment *attachment = [BITFeedbackMessageAttachment attachmentWithData:sourceAttachment.hockeyAttachmentData contentType:sourceAttachment.contentType]; + attachment.originalFilename = filename; + [self.attachments addObject:attachment]; + } else { + BITHockeyLogWarning(@"WARNING: Unknown item type %@", item); + } + } +} + + +#pragma mark - Keyboard + +- (void)keyboardWasShown:(NSNotification*)aNotification { + NSDictionary* info = [aNotification userInfo]; + CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size; + + BOOL isPortraitOrientation = UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation]); + + CGRect frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); + if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) { + frame.size.height -= kbSize.height; + } else { + CGSize windowSize = [[UIScreen mainScreen] bounds].size; + CGFloat windowHeight = windowSize.height - 20; + CGFloat navBarHeight = self.navigationController.navigationBar.frame.size.height; + + if (isPortraitOrientation) { + frame.size.height = windowHeight - navBarHeight - kbSize.height; + } else { + windowHeight = windowSize.height - 20; + CGFloat modalGap = 0.0; + if (windowHeight - kbSize.height < self.view.bounds.size.height) { + modalGap = 30; + } else { + modalGap = (windowHeight - self.view.bounds.size.height) / 2; + } + frame.size.height = windowSize.height - navBarHeight - modalGap - kbSize.height; + } + } + [self.contentViewContainer setFrame:frame]; + + [self performSelector:@selector(refreshAttachmentScrollview) withObject:nil afterDelay:0.0]; +} + +- (void)keyboardWillBeHidden:(NSNotification*) __unused aNotification { + CGRect frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); + [self.contentViewContainer setFrame:frame]; +} + + +#pragma mark - View lifecycle + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.title = BITHockeyLocalizedString(@"HockeyFeedbackComposeTitle"); + self.view.backgroundColor = [UIColor whiteColor]; + + // Do any additional setup after loading the view. + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel + target:self + action:@selector(dismissAction:)]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackComposeSend") + style:UIBarButtonItemStyleDone + target:self + action:@selector(sendAction:)]; + + // Container that contains both the textfield and eventually the photo scroll view on the right side + self.contentViewContainer = [[UIView alloc] initWithFrame:self.view.bounds]; + self.contentViewContainer.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; + + [self.view addSubview:self.contentViewContainer]; + + // message input textfield + self.textView = [[UITextView alloc] initWithFrame:self.view.bounds]; + self.textView.font = [UIFont systemFontOfSize:17]; + self.textView.delegate = self; + self.textView.backgroundColor = [UIColor whiteColor]; + self.textView.returnKeyType = UIReturnKeyDefault; + self.textView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; + self.textView.accessibilityHint = BITHockeyLocalizedString(@"HockeyAccessibilityHintRequired"); + + [self.contentViewContainer addSubview:self.textView]; + + // Add Photo Button + Container that's displayed above the keyboard. + if([BITHockeyHelper isPhotoAccessPossible]) { + self.textAccessoryView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 44)]; + self.textAccessoryView.backgroundColor = [UIColor colorWithRed:(CGFloat)0.9 green:(CGFloat)0.9 blue:(CGFloat)0.9 alpha:(CGFloat)1.0]; + + self.addPhotoButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [self.addPhotoButton setTitle:BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentAddImage") forState:UIControlStateNormal]; + [self.addPhotoButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal]; + [self.addPhotoButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateDisabled]; + self.addPhotoButton.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 44); + [self.addPhotoButton addTarget:self action:@selector(addPhotoAction:) forControlEvents:UIControlEventTouchUpInside]; + self.addPhotoButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin|UIViewAutoresizingFlexibleRightMargin; + [self.textAccessoryView addSubview:self.addPhotoButton]; + } + + + + if (!self.hideImageAttachmentButton) { + self.textView.inputAccessoryView = self.textAccessoryView; + } + + // This could be a subclass, yet + self.attachmentScrollView = [[UIScrollView alloc] initWithFrame:CGRectZero]; + self.attachmentScrollView.scrollEnabled = YES; + self.attachmentScrollView.bounces = YES; + self.attachmentScrollView.autoresizesSubviews = NO; + self.attachmentScrollView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleRightMargin; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (@available(iOS 11.0, *)) { + self.attachmentScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways; + } +#endif + [self.contentViewContainer addSubview:self.attachmentScrollView]; +} + +- (void)viewWillAppear:(BOOL)animated { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWasShown:) + name:UIKeyboardDidShowNotification object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillBeHidden:) + name:UIKeyboardWillHideNotification object:nil]; + + self.manager.currentFeedbackComposeViewController = self; + + [super viewWillAppear:animated]; + + if (self.text && self.textView.text.length == 0) { + self.textView.text = self.text; + } + + if (self.isStatusBarHiddenBeforeShowingPhotoPicker) { + [self setNeedsStatusBarAppearanceUpdate]; + } + + self.isStatusBarHiddenBeforeShowingPhotoPicker = nil; + + [self updateBarButtonState]; +} + +- (BOOL)prefersStatusBarHidden { + if (self.isStatusBarHiddenBeforeShowingPhotoPicker) { + return self.isStatusBarHiddenBeforeShowingPhotoPicker.boolValue; + } + + return [super prefersStatusBarHidden]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + BITFeedbackManager *strongManager = self.manager; + if ([strongManager askManualUserDataAvailable] && + ([strongManager requireManualUserDataMissing] || + ![strongManager didAskUserData]) + ) { + if (!self.blockUserDataScreen) + [self setUserDataAction]; + } else { + // Invoke delayed to fix iOS 7 iPad landscape bug, where this view will be moved if not called delayed + [self.textView performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0.0]; + [self refreshAttachmentScrollview]; + } +} + +- (void)viewWillDisappear:(BOOL)animated { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil]; + + self.manager.currentFeedbackComposeViewController = nil; + + [super viewWillDisappear:animated]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; +} + +- (void)refreshAttachmentScrollview { + CGFloat scrollViewWidth = 0; + + if (self.imageAttachments.count){ + scrollViewWidth = 100; + } + + CGRect textViewFrame = self.textView.frame; + + CGRect scrollViewFrame = self.attachmentScrollView.frame; + + BOOL alreadySetup = CGRectGetWidth(scrollViewFrame) > 0; + + if (alreadySetup && self.imageAttachments.count == 0) { + textViewFrame.size.width += 100; + self.textView.frame = textViewFrame; + scrollViewFrame.size.width = 0; + self.attachmentScrollView.frame = scrollViewFrame; + return; + } + + if (!alreadySetup) { + CGSize tempTextViewSize = CGSizeMake(self.contentViewContainer.frame.size.width, self.contentViewContainer.frame.size.height); + textViewFrame.size = tempTextViewSize; + textViewFrame.size.width -= scrollViewWidth; + // height has to be identical to the textview! + scrollViewFrame = CGRectMake(CGRectGetMaxX(textViewFrame), self.view.frame.origin.y, scrollViewWidth, CGRectGetHeight(self.textView.bounds)); + self.textView.frame = textViewFrame; + self.attachmentScrollView.frame = scrollViewFrame; + self.attachmentScrollView.contentInset = self.textView.contentInset; + } + + if (self.imageAttachments.count > self.attachmentScrollViewImageViews.count){ + NSInteger numberOfViewsToCreate = self.imageAttachments.count - self.attachmentScrollViewImageViews.count; + for (int i = 0; i 0 ) { + self.navigationItem.rightBarButtonItem.enabled = YES; + } else { + self.navigationItem.rightBarButtonItem.enabled = NO; + } + + if(self.addPhotoButton) { + if (self.imageAttachments.count > 2){ + [self.addPhotoButton setEnabled:NO]; + } else { + [self.addPhotoButton setEnabled:YES]; + } + } +} + +- (void)removeAttachmentScrollView { + CGRect frame = self.attachmentScrollView.frame; + frame.size.width = 0; + self.attachmentScrollView.frame = frame; + + frame = self.textView.frame; + frame.size.width += 100; + self.textView.frame = frame; +} + + +#pragma mark - UIViewController Rotation + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations{ + return UIInterfaceOrientationMaskAll; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation) __unused fromInterfaceOrientation { + [self removeAttachmentScrollView]; + + [self refreshAttachmentScrollview]; +} +#pragma clang diagnostic pop + +#pragma mark - Private methods + +- (void)setUserDataAction { + BITFeedbackUserDataViewController *userController = [[BITFeedbackUserDataViewController alloc] initWithStyle:UITableViewStyleGrouped]; + userController.delegate = self; + + UINavigationController *navController = [self.manager customNavigationControllerWithRootViewController:userController + presentationStyle:UIModalPresentationFormSheet]; + + [self presentViewController:navController animated:YES completion:nil]; +} + +#pragma mark - Actions + +- (void)dismissAction:(id) __unused sender { + for (BITFeedbackMessageAttachment *attachment in self.attachments){ + [attachment deleteContents]; + } + + [self dismissWithResult:BITFeedbackComposeResultCancelled]; +} + +- (void)sendAction:(id) __unused sender { + if ([self.textView isFirstResponder]) + [self.textView resignFirstResponder]; + + NSString *text = self.textView.text; + + [self.manager submitMessageWithText:text andAttachments:self.attachments]; + + [self dismissWithResult:BITFeedbackComposeResultSubmitted]; +} + +- (void)dismissWithResult:(BITFeedbackComposeResult) result { + id strongDelegate = self.delegate; + if([strongDelegate respondsToSelector:@selector(feedbackComposeViewController:didFinishWithResult:)]) { + [strongDelegate feedbackComposeViewController:self didFinishWithResult:result]; + } else { + [self dismissViewControllerAnimated:YES completion:nil]; + } +} + +- (void)addPhotoAction:(id) __unused sender { + if (self.actionSheetVisible) return; + + self.isStatusBarHiddenBeforeShowingPhotoPicker = @([[UIApplication sharedApplication] isStatusBarHidden]); + + // add photo. + UIImagePickerController *pickerController = [[UIImagePickerController alloc] init]; + pickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + pickerController.delegate = self; + pickerController.editing = NO; + pickerController.navigationBar.barStyle = self.manager.barStyle; + [self presentViewController:pickerController animated:YES completion:nil]; +} + +- (void)scrollViewTapped:(id) __unused unused { + UIMenuController *menuController = [UIMenuController sharedMenuController]; + [menuController setTargetRect:CGRectMake([self.tapRecognizer locationInView:self.view].x, [self.tapRecognizer locationInView:self.view].x, 1, 1) inView:self.view]; + [menuController setMenuVisible:YES animated:YES]; +} + +- (void)paste:(id) __unused sender { + +} + +#pragma mark - UIImagePickerControllerDelegate + +- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { + UIImage *pickedImage = info[UIImagePickerControllerOriginalImage]; + + if (pickedImage){ + NSData *imageData = UIImageJPEGRepresentation(pickedImage, (CGFloat)0.7); + BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment attachmentWithData:imageData contentType:@"image/jpeg"]; + NSURL *imagePath = [info objectForKey:@"UIImagePickerControllerReferenceURL"]; + NSString *imageName = [imagePath lastPathComponent]; + newAttachment.originalFilename = imageName; + [self.attachments addObject:newAttachment]; + [self.imageAttachments addObject:newAttachment]; + } + + [picker dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { + [picker dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)imageButtonAction:(UIButton *)sender { + // determine the index of the feedback + NSInteger index = [self.attachmentScrollViewImageViews indexOfObject:sender]; + + self.selectedAttachmentIndex = (self.attachmentScrollViewImageViews.count - index - 1); + /* We won't use this for now until we have a more robust solution for displaying UIAlertController + // requires iOS 8 + id uialertcontrollerClass = NSClassFromString(@"UIAlertController"); + if (uialertcontrollerClass) { + __weak typeof(self) weakSelf = self; + + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil + message:nil + preferredStyle:UIAlertControllerStyleActionSheet]; + + + UIAlertAction *cancelAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentCancel") + style:UIAlertActionStyleCancel + handler:^(UIAlertAction * action) { + typeof(self) strongSelf = weakSelf; + [strongSelf cancelAction]; + _actionSheetVisible = NO; + }]; + + [alertController addAction:cancelAction]; + + UIAlertAction *editAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentEdit") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + typeof(self) strongSelf = weakSelf; + [strongSelf editAction]; + _actionSheetVisible = NO; + }]; + + [alertController addAction:editAction]; + + UIAlertAction *deleteAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentDelete") + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction * action) { + typeof(self) strongSelf = weakSelf; + [strongSelf deleteAction]; + _actionSheetVisible = NO; + }]; + + [alertController addAction:deleteAction]; + + [self presentViewController:alertController animated:YES completion:nil]; + } else { + */ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle: nil + delegate: self + cancelButtonTitle: BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentCancel") + destructiveButtonTitle: BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentDelete") + otherButtonTitles: BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentEdit"), nil]; + + [actionSheet showFromRect: sender.frame inView: self.attachmentScrollView animated: YES]; +#pragma clang diagnostic push + /*}*/ + + self.actionSheetVisible = YES; + if ((UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) || ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){9,0,0}])) { + [self.textView resignFirstResponder]; + } +} + + +#pragma mark - BITFeedbackUserDataDelegate + +- (void)userDataUpdateCancelled { + self.blockUserDataScreen = YES; + + if ([self.manager requireManualUserDataMissing]) { + if ([self.navigationController respondsToSelector:@selector(dismissViewControllerAnimated:completion:)]) { + [self.navigationController dismissViewControllerAnimated:YES + completion:^(void) { + [self dismissViewControllerAnimated:YES completion:nil]; + }]; + } else { + [self dismissViewControllerAnimated:YES completion:nil]; + [self performSelector:@selector(dismissAction:) withObject:nil afterDelay:0.4]; + } + } else { + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; + } +} + +- (void)userDataUpdateFinished { + [self.manager saveMessages]; + + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; +} + + +#pragma mark - UITextViewDelegate + +- (void)textViewDidChange:(UITextView *) __unused textView { + [self updateBarButtonState]; +} + + +#pragma mark - UIActionSheet Delegate + +- (void)deleteAction { + if (self.selectedAttachmentIndex != NSNotFound){ + UIButton *imageButton = self.attachmentScrollViewImageViews[self.selectedAttachmentIndex]; + BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex]; + [attachment deleteContents]; // mandatory call to delete the files associated. + [self.imageAttachments removeObject:attachment]; + [self.attachments removeObject:attachment]; + [imageButton removeFromSuperview]; + [self.attachmentScrollViewImageViews removeObject:imageButton]; + } + self.selectedAttachmentIndex = NSNotFound; + + [self refreshAttachmentScrollview]; + + if ((UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) || ([[NSProcessInfo processInfo] respondsToSelector:@selector(isOperatingSystemAtLeastVersion:)] && [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){9,0,0}])) { + [self.textView becomeFirstResponder]; + } +} + +- (void)editAction { + if (self.selectedAttachmentIndex != NSNotFound){ + BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex]; + BITImageAnnotationViewController *annotationEditor = [[BITImageAnnotationViewController alloc ] init]; + annotationEditor.delegate = self; + UINavigationController *navController = [self.manager customNavigationControllerWithRootViewController:annotationEditor presentationStyle:UIModalPresentationFullScreen]; + annotationEditor.image = attachment.imageRepresentation; + [self presentViewController:navController animated:YES completion:nil]; + } +} + +- (void)cancelAction { + if ((UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) || ([[NSProcessInfo processInfo] respondsToSelector:@selector(isOperatingSystemAtLeastVersion:)] && [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){9,0,0}])) { + [self.textView becomeFirstResponder]; + } +} + +- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex { + if (buttonIndex == [actionSheet destructiveButtonIndex]) { + [self deleteAction]; + } else if (buttonIndex != [actionSheet cancelButtonIndex]) { + [self editAction]; + } else { + [self cancelAction]; + } + self.actionSheetVisible = NO; +} + + +#pragma mark - Image Annotation Delegate + +- (void)annotationController:(BITImageAnnotationViewController *) __unused annotationController didFinishWithImage:(UIImage *)image { + if (self.selectedAttachmentIndex != NSNotFound){ + BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex]; + [attachment replaceData:UIImageJPEGRepresentation(image, (CGFloat)0.7)]; + } + + self.selectedAttachmentIndex = NSNotFound; +} + +- (void)annotationControllerDidCancel:(BITImageAnnotationViewController *) __unused annotationController { + self.selectedAttachmentIndex = NSNotFound; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackComposeViewControllerDelegate.h b/submodules/HockeySDK-iOS/Classes/BITFeedbackComposeViewControllerDelegate.h new file mode 100644 index 0000000000..99bac6cf84 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackComposeViewControllerDelegate.h @@ -0,0 +1,76 @@ +/* + * 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 + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +/** + * The users action when composing a message + */ +typedef NS_ENUM(NSUInteger, BITFeedbackComposeResult) { + /** + * user hit cancel + */ + BITFeedbackComposeResultCancelled, + /** + * user hit submit + */ + BITFeedbackComposeResultSubmitted, +}; + +@class BITFeedbackComposeViewController; + +/** + * The `BITFeedbackComposeViewControllerDelegate` formal protocol defines methods further configuring + * the behaviour of `BITFeedbackComposeViewController`. + */ + +@protocol BITFeedbackComposeViewControllerDelegate + +@optional + +///----------------------------------------------------------------------------- +/// @name View Controller Management +///----------------------------------------------------------------------------- + +/** + * Invoked once the compose screen is finished via send or cancel + * + * If this is implemented, it's the responsibility of this method to dismiss the presented + * `BITFeedbackComposeViewController` + * + * @param composeViewController The `BITFeedbackComposeViewController` instance invoking this delegate + * @param composeResult The user action the lead to closing the compose view + */ +- (void)feedbackComposeViewController:(BITFeedbackComposeViewController *)composeViewController + didFinishWithResult:(BITFeedbackComposeResult) composeResult; +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackListViewCell.h b/submodules/HockeySDK-iOS/Classes/BITFeedbackListViewCell.h new file mode 100644 index 0000000000..f8947a50ec --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackListViewCell.h @@ -0,0 +1,71 @@ +/* + * 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 +#import "BITFeedbackMessage.h" +#import "BITAttributedLabel.h" + +@class BITFeedbackMessageAttachment; + +@protocol BITFeedbackListViewCellDelegate + +- (void)listCell:(id)cell didSelectAttachment:(BITFeedbackMessageAttachment *)attachment; + +@end + +/** + * Cell background style + */ +typedef NS_ENUM(NSUInteger, BITFeedbackListViewCellBackgroundStyle) { + /** + * For even rows + */ + BITFeedbackListViewCellBackgroundStyleNormal = 0, + /** + * For uneven rows + */ + BITFeedbackListViewCellBackgroundStyleAlternate = 1 +}; + + +@interface BITFeedbackListViewCell : UITableViewCell + +@property (nonatomic, strong) BITFeedbackMessage *message; + +@property (nonatomic) BITFeedbackListViewCellBackgroundStyle backgroundStyle; + +@property (nonatomic, strong) BITAttributedLabel *labelText; + +@property (nonatomic, weak) id delegate; + ++ (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width; + +- (void)setAttachments:(NSArray *)attachments; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackListViewCell.m b/submodules/HockeySDK-iOS/Classes/BITFeedbackListViewCell.m new file mode 100644 index 0000000000..59d661ad37 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackListViewCell.m @@ -0,0 +1,362 @@ +/* + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "HockeySDKPrivate.h" + +#import "BITFeedbackListViewCell.h" +#import "BITFeedbackMessageAttachment.h" +#import "BITActivityIndicatorButton.h" +#import "BITFeedbackManagerPrivate.h" + +#import + +#define BACKGROUNDCOLOR_DEFAULT_OS7 BIT_RGBCOLOR(255, 255, 255) +#define BACKGROUNDCOLOR_ALTERNATE_OS7 BIT_RGBCOLOR(255, 255, 255) + +#define TEXTCOLOR_TITLE BIT_RGBCOLOR(75, 75, 75) + +#define TEXTCOLOR_DEFAULT BIT_RGBCOLOR(25, 25, 25) +#define TEXTCOLOR_PENDING BIT_RGBCOLOR(75, 75, 75) + +#define TITLE_FONTSIZE 12 +#define TEXT_FONTSIZE 15 + +#define FRAME_SIDE_BORDER 10 +#define FRAME_TOP_BORDER 8 +#define FRAME_BOTTOM_BORDER 5 + +#define LABEL_TITLE_Y 3 +#define LABEL_TITLE_HEIGHT 15 + +#define LABEL_TEXT_Y 25 + +#define ATTACHMENT_SIZE 45 + + +@interface BITFeedbackListViewCell () + +@property (nonatomic, strong) NSDateFormatter *dateFormatter; +@property (nonatomic, strong) NSDateFormatter *timeFormatter; + +@property (nonatomic, strong) UILabel *labelTitle; + +@property (nonatomic, strong) NSMutableArray *attachmentViews; + +@property (nonatomic, strong) UIView *accessoryBackgroundView; + +@property (nonatomic, strong) id updateAttachmentNotification; + +@end + + +@implementation BITFeedbackListViewCell + + +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + if (self) { + // Initialization code + _backgroundStyle = BITFeedbackListViewCellBackgroundStyleNormal; + + _message = nil; + + _dateFormatter = [[NSDateFormatter alloc] init]; + [_dateFormatter setTimeStyle:NSDateFormatterNoStyle]; + [_dateFormatter setDateStyle:NSDateFormatterMediumStyle]; + [_dateFormatter setLocale:[NSLocale currentLocale]]; + [_dateFormatter setDoesRelativeDateFormatting:YES]; + + _timeFormatter = [[NSDateFormatter alloc] init]; + [_timeFormatter setTimeStyle:NSDateFormatterShortStyle]; + [_timeFormatter setDateStyle:NSDateFormatterNoStyle]; + [_timeFormatter setLocale:[NSLocale currentLocale]]; + [_timeFormatter setDoesRelativeDateFormatting:YES]; + + _labelTitle = [[UILabel alloc] init]; + _labelTitle.font = [UIFont systemFontOfSize:TITLE_FONTSIZE]; + + _labelText = [[BITAttributedLabel alloc] initWithFrame:CGRectZero]; + _labelText.font = [UIFont systemFontOfSize:TEXT_FONTSIZE]; + _labelText.numberOfLines = 0; + _labelText.textAlignment = NSTextAlignmentLeft; + _labelText.enabledTextCheckingTypes = UIDataDetectorTypeAll; + + _attachmentViews = [NSMutableArray new]; + [self registerObservers]; + } + return self; +} + +- (void)dealloc { + [self unregisterObservers]; +} + + +#pragma mark - Private + +- (void) registerObservers { + __weak typeof(self) weakSelf = self; + if (nil == self.updateAttachmentNotification) { + self.updateAttachmentNotification = [[NSNotificationCenter defaultCenter] addObserverForName:kBITFeedbackUpdateAttachmentThumbnail + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf updateAttachmentFromNotification:note]; + }]; + } +} + +- (void) unregisterObservers { + if (self.updateAttachmentNotification) { + [[NSNotificationCenter defaultCenter] removeObserver:self.updateAttachmentNotification]; + self.updateAttachmentNotification = nil; + } +} + +- (void) updateAttachmentFromNotification:(NSNotification *)note { + if (!self.message) return; + if (!self.message.attachments) return; + if (self.message.attachments.count == 0) return; + if (!note.object) return; + if (![note.object isKindOfClass:[BITFeedbackMessageAttachment class]]) return; + + BITFeedbackMessageAttachment *attachment = (BITFeedbackMessageAttachment *)note.object; + if (![self.message.attachments containsObject:attachment]) return; + + // The attachment is part of the message used for this cell, so lets update it. + [self setAttachments:self.message.previewableAttachments]; + [self setNeedsLayout]; +} + +- (UIColor *)backgroundColor { + + if (self.backgroundStyle == BITFeedbackListViewCellBackgroundStyleNormal) { + return BACKGROUNDCOLOR_DEFAULT_OS7; + } else { + return BACKGROUNDCOLOR_ALTERNATE_OS7; + } +} + +- (BOOL)isSameDayWithDate1:(NSDate*)date1 date2:(NSDate*)date2 { + NSCalendar* calendar = [NSCalendar currentCalendar]; + + unsigned unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay; + + NSDateComponents *dateComponent1 = [calendar components:unitFlags fromDate:date1]; + NSDateComponents *dateComponent2 = [calendar components:unitFlags fromDate:date2]; + + return ([dateComponent1 day] == [dateComponent2 day] && + [dateComponent1 month] == [dateComponent2 month] && + [dateComponent1 year] == [dateComponent2 year]); +} + + +#pragma mark - Layout + ++ (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width { + + CGFloat baseHeight = [self heightForTextInRowWithMessage:message tableViewWidth:width]; + + CGFloat attachmentsPerRow = floor(width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); + + CGFloat calculatedHeight = baseHeight + (FRAME_TOP_BORDER + ATTACHMENT_SIZE) * ceil([message previewableAttachments].count / attachmentsPerRow); + + return ceil(calculatedHeight); +} + + + ++ (CGFloat) heightForTextInRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width { + CGFloat calculatedHeight; + + if ([message.text respondsToSelector:@selector(boundingRectWithSize:options:attributes:context:)]) { + CGRect calculatedRect = [message.text boundingRectWithSize:CGSizeMake(width - (2 * FRAME_SIDE_BORDER), CGFLOAT_MAX) + options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading + attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:TEXT_FONTSIZE]} + context:nil]; + calculatedHeight = calculatedRect.size.height + FRAME_TOP_BORDER + LABEL_TEXT_Y + FRAME_BOTTOM_BORDER; + + // added to make space for the images. + + + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + calculatedHeight = [message.text sizeWithFont:[UIFont systemFontOfSize:TEXT_FONTSIZE] + constrainedToSize:CGSizeMake(width - (2 * FRAME_SIDE_BORDER), CGFLOAT_MAX) + ].height + FRAME_TOP_BORDER + LABEL_TEXT_Y + FRAME_BOTTOM_BORDER; + +#pragma clang diagnostic pop + } + + return ceil(calculatedHeight); +} + +- (void)setAttachments:(NSArray *)attachments { + for (UIView *view in self.attachmentViews){ + [view removeFromSuperview]; + } + + [self.attachmentViews removeAllObjects]; + + for (BITFeedbackMessageAttachment *attachment in attachments){ + if (attachment.localURL || attachment.sourceURL) { + BITActivityIndicatorButton *imageView = [BITActivityIndicatorButton buttonWithType:UIButtonTypeCustom]; + + if (attachment.localURL){ + [imageView setImage:[attachment thumbnailWithSize:CGSizeMake(ATTACHMENT_SIZE, ATTACHMENT_SIZE)] forState:UIControlStateNormal]; + [imageView setShowsActivityIndicator:NO]; + } else { + [imageView setImage:nil forState:UIControlStateNormal]; + [imageView setShowsActivityIndicator:YES]; + } + [imageView setContentMode:UIViewContentModeScaleAspectFit]; + [imageView addTarget:self action:@selector(imageButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + + [self.attachmentViews addObject:imageView]; + } + } +} + + +- (void)layoutSubviews { + if (!self.accessoryBackgroundView){ + self.accessoryBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 2, self.frame.size.width * 2, self.frame.size.height - 2)]; + self.accessoryBackgroundView.autoresizingMask = UIViewAutoresizingFlexibleHeight; + self.accessoryBackgroundView.clipsToBounds = YES; + + // colors + self.accessoryBackgroundView.backgroundColor = [self backgroundColor]; + } + + if (self.accessoryBackgroundView.superview){ + [self.accessoryBackgroundView removeFromSuperview]; + } + self.contentView.backgroundColor = [self backgroundColor]; + self.labelTitle.backgroundColor = [self backgroundColor]; + self.labelText.backgroundColor = [self backgroundColor]; + + self.labelTitle.textColor = TEXTCOLOR_TITLE; + if (self.message.status == BITFeedbackMessageStatusSendPending || self.message.status == BITFeedbackMessageStatusSendInProgress) { + [self.labelText setTextColor:TEXTCOLOR_PENDING]; + } else { + [self.labelText setTextColor:TEXTCOLOR_DEFAULT]; + } + + // background for deletion accessory view + + + // header + NSString *dateString = @""; + if (self.message.status == BITFeedbackMessageStatusSendPending || self.message.status == BITFeedbackMessageStatusSendInProgress) { + dateString = BITHockeyLocalizedString(@"Pending"); + } else if (self.message.date) { + if ([self isSameDayWithDate1:[NSDate date] date2:self.message.date]) { + dateString = [self.timeFormatter stringFromDate:self.message.date]; + } else { + dateString = [self.dateFormatter stringFromDate:self.message.date]; + } + } + [self.labelTitle setText:dateString]; + [self.labelTitle setFrame:CGRectMake(FRAME_SIDE_BORDER, FRAME_TOP_BORDER + LABEL_TITLE_Y, self.frame.size.width - (2 * FRAME_SIDE_BORDER), LABEL_TITLE_HEIGHT)]; + + if (self.message.userMessage) { + self.labelTitle.textAlignment = NSTextAlignmentRight; + self.labelText.textAlignment = NSTextAlignmentRight; + } else { + self.labelTitle.textAlignment = NSTextAlignmentLeft; + self.labelText.textAlignment = NSTextAlignmentLeft; + } + + [self addSubview:self.labelTitle]; + + // text + [self.labelText setText:self.message.text]; + CGSize sizeForTextLabel = CGSizeMake(self.frame.size.width - (2 * FRAME_SIDE_BORDER), + [[self class] heightForTextInRowWithMessage:self.message tableViewWidth:self.frame.size.width] - LABEL_TEXT_Y - FRAME_BOTTOM_BORDER); + + [self.labelText setFrame:CGRectMake(FRAME_SIDE_BORDER, LABEL_TEXT_Y, sizeForTextLabel.width, sizeForTextLabel.height)]; + + [self addSubview:self.labelText]; + + CGFloat baseOffsetOfText = CGRectGetMaxY(self.labelText.frame); + + + int i = 0; + + CGFloat attachmentsPerRow = floor(self.frame.size.width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); + + for (BITActivityIndicatorButton *imageButton in self.attachmentViews) { + imageButton.contentMode = UIViewContentModeScaleAspectFit; + imageButton.imageView.contentMode = UIViewContentModeScaleAspectFill; + + if (!self.message.userMessage) { + imageButton.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i%(int)attachmentsPerRow) , floor(i/attachmentsPerRow)*(FRAME_SIDE_BORDER + ATTACHMENT_SIZE) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); + } else { + imageButton.frame = CGRectMake(self.frame.size.width - FRAME_SIDE_BORDER - ATTACHMENT_SIZE - ((FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i%(int)attachmentsPerRow) ), floor(i/attachmentsPerRow)*(FRAME_SIDE_BORDER + ATTACHMENT_SIZE) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); + } + + if (!imageButton.superview) { + if (self.accessoryBackgroundView.superview) { + [self insertSubview:imageButton aboveSubview:self.accessoryBackgroundView]; + } else { + [self addSubview:imageButton]; + } + } + + i++; + } + + [super layoutSubviews]; +} + +- (void)imageButtonPressed:(id)sender { + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(listCell:didSelectAttachment:)]) { + NSUInteger index = [self.attachmentViews indexOfObject:sender]; + if (index != NSNotFound && [self.message previewableAttachments].count > index) { + BITFeedbackMessageAttachment *attachment = [self.message previewableAttachments][index]; + [strongDelegate listCell:self didSelectAttachment:attachment]; + } + } +} + +- (NSString *)accessibilityLabel { + NSString *messageTime = [self.labelTitle accessibilityLabel]; + NSString *messageText = [self.labelText accessibilityLabel]; + return [NSString stringWithFormat:@"%@, %@", messageTime, messageText]; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackListViewController.h b/submodules/HockeySDK-iOS/Classes/BITFeedbackListViewController.h new file mode 100644 index 0000000000..62fd91d518 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackListViewController.h @@ -0,0 +1,61 @@ +/* + * 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 +#import + +#import "BITHockeyBaseViewController.h" + +/** + View controller providing a default interface to manage feedback + + The message list interface contains options to locally delete single messages + by swiping over them, or deleting all messages. This will not delete the messages + on the server though! + + It is also integrates actions to invoke the user interface to compose a new messages, + reload the list content from the server and changing the users name or email if these + are allowed to be set. + + To add this view controller to your own app and push it onto a navigation stack, + don't create the instance yourself, but use the following code to get a correct instance: + + [[BITHockeyManager sharedHockeyManager].feedbackManager feedbackListViewController:NO] + + To show it modally, use the following code instead: + + [[BITHockeyManager sharedHockeyManager].feedbackManager feedbackListViewController:YES] + + This ensures that the presentation on iOS 6 and iOS 7 will use the current design on each OS Version. + */ + +@interface BITFeedbackListViewController : BITHockeyBaseViewController { +} + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackListViewController.m b/submodules/HockeySDK-iOS/Classes/BITFeedbackListViewController.m new file mode 100644 index 0000000000..14a1dcbc98 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackListViewController.m @@ -0,0 +1,804 @@ +/* + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "HockeySDKPrivate.h" + +#import "BITFeedbackManagerPrivate.h" +#import "BITFeedbackManager.h" +#import "BITFeedbackListViewController.h" +#import "BITFeedbackListViewCell.h" +#import "BITFeedbackComposeViewController.h" +#import "BITFeedbackUserDataViewController.h" +#import "BITFeedbackMessage.h" +#import "BITFeedbackMessageAttachment.h" +#import "BITAttributedLabel.h" + +#import "BITHockeyBaseManagerPrivate.h" + +#import "BITHockeyHelper.h" +#import +#import + +#define DEFAULT_TEXTCOLOR BIT_RGBCOLOR(75, 75, 75) + +#define BORDER_COLOR BIT_RGBCOLOR(215, 215, 215) + + +@interface BITFeedbackListViewController () + +@property (nonatomic, weak) BITFeedbackManager *manager; +@property (nonatomic, strong) NSDateFormatter *lastUpdateDateFormatter; +@property (nonatomic) BOOL userDataComposeFlow; +@property (nonatomic, strong) NSArray *cachedPreviewItems; +@property (nonatomic, strong) NSOperationQueue *thumbnailQueue; +@property (nonatomic) NSInteger deleteButtonSection; +@property (nonatomic) NSInteger userButtonSection; +@property (nonatomic) NSInteger numberOfSectionsBeforeRotation; +@property (nonatomic) NSInteger numberOfMessagesBeforeRotation; + +@end + + +@implementation BITFeedbackListViewController + +- (instancetype)initWithStyle:(UITableViewStyle)style { + if ((self = [super initWithStyle:style])) { + _manager = [BITHockeyManager sharedHockeyManager].feedbackManager; + + _deleteButtonSection = -1; + self.userButtonSection = -1; + _userDataComposeFlow = NO; + + _numberOfSectionsBeforeRotation = -1; + _numberOfMessagesBeforeRotation = -1; + + + _lastUpdateDateFormatter = [[NSDateFormatter alloc] init]; + [_lastUpdateDateFormatter setDateStyle:NSDateFormatterShortStyle]; + [_lastUpdateDateFormatter setTimeStyle:NSDateFormatterShortStyle]; + _lastUpdateDateFormatter.locale = [NSLocale currentLocale]; + + _thumbnailQueue = [NSOperationQueue new]; + } + return self; +} + + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyFeedbackMessagesLoadingStarted object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyFeedbackMessagesLoadingFinished object:nil]; + + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(showDelayedUserDataViewController) object:nil]; +} + + +#pragma mark - View lifecycle + +- (void)viewDidLoad { + [super viewDidLoad]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(startLoadingIndicator) + name:BITHockeyFeedbackMessagesLoadingStarted + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateList) + name:BITHockeyFeedbackMessagesLoadingFinished + object:nil]; + + self.title = BITHockeyLocalizedString(@"HockeyFeedbackListTitle"); + + self.tableView.delegate = self; + self.tableView.dataSource = self; + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + [self.tableView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth]; + + if ([UIRefreshControl class]) { + self.refreshControl = [[UIRefreshControl alloc] init]; + [self.refreshControl addTarget:self action:@selector(reloadList) forControlEvents:UIControlEventValueChanged]; + } else { + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh + target:self + action:@selector(reloadList)]; + } +} + +- (void)startLoadingIndicator { + if ([UIRefreshControl class]) { + [self.refreshControl beginRefreshing]; + } else { + self.navigationItem.rightBarButtonItem.enabled = NO; + } +} + +- (void)stopLoadingIndicator { + if ([UIRefreshControl class]) { + [self.refreshControl endRefreshing]; + } else { + self.navigationItem.rightBarButtonItem.enabled = YES; + } +} + +- (BOOL)isRefreshingWithNewControl { + if ([UIRefreshControl class]) { + return [self.refreshControl isRefreshing]; + } + return NO; +} + +- (void)reloadList { + [self startLoadingIndicator]; + + [self.manager updateMessagesList]; +} + +- (void)updateList { + dispatch_async(dispatch_get_main_queue(), ^{ + CGSize contentSize = self.tableView.contentSize; + CGPoint contentOffset = self.tableView.contentOffset; + + [self refreshPreviewItems]; + [self.tableView reloadData]; + + if (contentSize.height > 0 && + self.tableView.contentSize.height > self.tableView.frame.size.height && + self.tableView.contentSize.height > contentSize.height && + ![self isRefreshingWithNewControl]) + [self.tableView setContentOffset:CGPointMake(contentOffset.x, self.tableView.contentSize.height - contentSize.height + contentOffset.y) animated:NO]; + + [self stopLoadingIndicator]; + + [self.tableView flashScrollIndicators]; + }); +} + +- (void)viewDidAppear:(BOOL)animated { + if (self.userDataComposeFlow) { + self.userDataComposeFlow = NO; + } + BITFeedbackManager *strongManager = self.manager; + strongManager.currentFeedbackListViewController = self; + + [strongManager updateMessagesListIfRequired]; + + if ([strongManager numberOfMessages] == 0 && + [strongManager askManualUserDataAvailable] && + [strongManager requireManualUserDataMissing] && + ![strongManager didAskUserData] + ) { + self.userDataComposeFlow = YES; + + if ([strongManager showFirstRequiredPresentationModal]) { + [self setUserDataAction:nil]; + } else { + // In case of presenting the feedback in a UIPopoverController it appears + // that the animation is not yet finished (though it should) and pushing + // the user data view on top of the navigation stack right away will + // cause the following warning to appear in the console: + // "nested push animation can result in corrupted navigation bar" + [self performSelector:@selector(showDelayedUserDataViewController) withObject:nil afterDelay:0.0]; + } + } else { + [self.tableView reloadData]; + } + + [super viewDidAppear:animated]; +} + +- (void)viewWillDisappear:(BOOL)animated { + self.manager.currentFeedbackListViewController = nil; + + [super viewWillDisappear:animated]; +} + + +#pragma mark - Private methods + +- (void)showDelayedUserDataViewController { + BITFeedbackUserDataViewController *userController = [[BITFeedbackUserDataViewController alloc] initWithStyle:UITableViewStyleGrouped]; + userController.delegate = self; + + [self.navigationController pushViewController:userController animated:YES]; +} + +- (void)setUserDataAction:(id) __unused sender { + BITFeedbackUserDataViewController *userController = [[BITFeedbackUserDataViewController alloc] initWithStyle:UITableViewStyleGrouped]; + userController.delegate = self; + + UINavigationController *navController = [self.manager customNavigationControllerWithRootViewController:userController + presentationStyle:UIModalPresentationFormSheet]; + + [self presentViewController:navController animated:YES completion:nil]; +} + +- (void)newFeedbackAction:(id) __unused sender { + BITFeedbackManager *strongManager = self.manager; + BITFeedbackComposeViewController *composeController = [strongManager feedbackComposeViewController]; + + UINavigationController *navController = [strongManager customNavigationControllerWithRootViewController:composeController + presentationStyle:UIModalPresentationFormSheet]; + + [self presentViewController:navController animated:YES completion:nil]; +} + +- (void)deleteAllMessages { + [self.manager deleteAllMessages]; + [self refreshPreviewItems]; + + [self.tableView reloadData]; +} + +- (void)deleteAllMessagesAction:(id) __unused sender { + NSString *title = BITHockeyLocalizedString(@"HockeyFeedbackListButtonDeleteAllMessages"); + NSString *message = BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllTitle"); + UIAlertControllerStyle controllerStyle = UIAlertControllerStyleAlert; + if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) { + controllerStyle = UIAlertControllerStyleActionSheet; + title = BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllTitle"); + message = nil; + } + __weak typeof(self) weakSelf = self; + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title + message:message + preferredStyle:controllerStyle]; + UIAlertAction* cancelAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllCancel") + style:UIAlertActionStyleCancel + handler:^(UIAlertAction __unused *action) {}]; + [alertController addAction:cancelAction]; + UIAlertAction* deleteAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllDelete") + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + [strongSelf deleteAllMessages]; + }]; + [alertController addAction:deleteAction]; + [self presentViewController:alertController animated:YES completion:nil]; +} + +- (UIView*) viewForShowingActionSheetOnPhone { + //find the topmost presented view controller + //and use its view + UIViewController* topMostPresentedViewController = self.view.window.rootViewController; + while(topMostPresentedViewController.presentedViewController) { + topMostPresentedViewController = topMostPresentedViewController.presentedViewController; + } + UIView* view = topMostPresentedViewController.view; + + if(nil == view) { + //hope for the best. Should work + //on simple view(controller) hierarchies + view = self.view; + } + + return view; +} + +#pragma mark - BITFeedbackUserDataDelegate + +-(void)userDataUpdateCancelled { + if (self.userDataComposeFlow) { + if ([self.manager showFirstRequiredPresentationModal]) { + __weak typeof(self) weakSelf = self; + [self dismissViewControllerAnimated:YES completion:^(void){ + typeof(self) strongSelf = weakSelf; + [strongSelf.tableView reloadData]; + }]; + } else { + [self.navigationController popToViewController:self animated:YES]; + } + } else { + [self dismissViewControllerAnimated:YES completion:^(void){}]; + } +} + +-(void)userDataUpdateFinished { + BITFeedbackManager *strongManager = self.manager; + [strongManager saveMessages]; + [self refreshPreviewItems]; + + if (self.userDataComposeFlow) { + if ([strongManager showFirstRequiredPresentationModal]) { + __weak typeof(self) weakSelf = self; + [self dismissViewControllerAnimated:YES completion:^(void){ + typeof(self) strongSelf = weakSelf; + [strongSelf newFeedbackAction:nil]; + }]; + } else { + BITFeedbackComposeViewController *composeController = [[BITFeedbackComposeViewController alloc] init]; + composeController.delegate = self; + + [self.navigationController pushViewController:composeController animated:YES]; + } + } else { + [self dismissViewControllerAnimated:YES completion:^(void){}]; + } +} + + +#pragma mark - BITFeedbackComposeViewControllerDelegate + +- (void)feedbackComposeViewController:(BITFeedbackComposeViewController *)composeViewController + didFinishWithResult:(BITFeedbackComposeResult)composeResult { + BITFeedbackManager *strongManager = self.manager; + if (self.userDataComposeFlow) { + if ([strongManager showFirstRequiredPresentationModal]) { + __weak typeof(self) weakSelf = self; + [self dismissViewControllerAnimated:YES completion:^(void){ + typeof(self) strongSelf = weakSelf; + [strongSelf.tableView reloadData]; + }]; + } else { + [self.navigationController popToViewController:self animated:YES]; + } + } else { + [self dismissViewControllerAnimated:YES completion:^(void){}]; + } + id strongDelegate = strongManager.delegate; + if ([strongDelegate respondsToSelector:@selector(feedbackComposeViewController:didFinishWithResult:)]) { + [strongDelegate feedbackComposeViewController:composeViewController didFinishWithResult:composeResult]; + } +} + + +#pragma mark - UIViewController Rotation + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { + self.numberOfSectionsBeforeRotation = [self numberOfSectionsInTableView:self.tableView]; + self.numberOfMessagesBeforeRotation = [self.manager numberOfMessages]; + [self.tableView reloadData]; + [self.tableView beginUpdates]; + [self.tableView endUpdates]; + + self.numberOfSectionsBeforeRotation = -1; + self.numberOfMessagesBeforeRotation = -1; + [self.tableView reloadData]; + + [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; +} +#pragma clang diagnostic pop + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations{ + return UIInterfaceOrientationMaskAll; +} + +#pragma mark - Table view data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView *) __unused tableView { + if (self.numberOfSectionsBeforeRotation >= 0) + return self.numberOfSectionsBeforeRotation; + + NSInteger sections = 2; + self.deleteButtonSection = -1; + self.userButtonSection = -1; + BITFeedbackManager *strongManager = self.manager; + if ([strongManager isManualUserDataAvailable] || [strongManager didAskUserData]) { + self.userButtonSection = sections; + sections++; + } + + if ([strongManager numberOfMessages] > 0) { + self.deleteButtonSection = sections; + sections++; + } + + return sections; +} + +- (NSInteger)tableView:(UITableView *) __unused tableView numberOfRowsInSection:(NSInteger)section { + if (section == 1) { + if (self.numberOfMessagesBeforeRotation >= 0) + return self.numberOfMessagesBeforeRotation; + return [self.manager numberOfMessages]; + } else { + return 1; + } +} + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { + if (section == 0) { + return 30; + } + return [super tableView:tableView heightForHeaderInSection:section]; +} + +- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { + if (section == 0) { + BITFeedbackManager *strongManager = self.manager; + UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 30.0)]; + UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectMake(16.0, 5.0, self.view.frame.size.width - (CGFloat)32.0, 25.0)]; + textLabel.text = [NSString stringWithFormat:BITHockeyLocalizedString(@"HockeyFeedbackListLastUpdated"), + [strongManager lastCheck] ? [self.lastUpdateDateFormatter stringFromDate:[strongManager lastCheck]] : BITHockeyLocalizedString(@"HockeyFeedbackListNeverUpdated")]; + textLabel.font = [UIFont systemFontOfSize:10]; + textLabel.textColor = DEFAULT_TEXTCOLOR; + [containerView addSubview:textLabel]; + + return containerView; + } + + return [super tableView:tableView viewForHeaderInSection:section]; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *CellIdentifier = @"MessageCell"; + static NSString *LastUpdateIdentifier = @"LastUpdateCell"; + static NSString *ButtonTopIdentifier = @"ButtonTopCell"; + static NSString *ButtonBottomIdentifier = @"ButtonBottomCell"; + static NSString *ButtonDeleteIdentifier = @"ButtonDeleteCell"; + BITFeedbackManager *strongManager = self.manager; + if (indexPath.section == 0 && indexPath.row == 1) { + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:LastUpdateIdentifier]; + + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:LastUpdateIdentifier]; + cell.textLabel.font = [UIFont systemFontOfSize:10]; + cell.textLabel.textColor = DEFAULT_TEXTCOLOR; + cell.accessoryType = UITableViewCellAccessoryNone; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + cell.textLabel.textAlignment = NSTextAlignmentCenter; + } + cell.textLabel.accessibilityTraits = UIAccessibilityTraitStaticText; + cell.textLabel.text = [NSString stringWithFormat:BITHockeyLocalizedString(@"HockeyFeedbackListLastUpdated"), + [strongManager lastCheck] ? [self.lastUpdateDateFormatter stringFromDate:[strongManager lastCheck]] : BITHockeyLocalizedString(@"HockeyFeedbackListNeverUpdated")]; + + return cell; + } else if (indexPath.section == 0 || indexPath.section >= 2) { + UITableViewCell *cell = nil; + + NSString *identifier = nil; + if (indexPath.section == 0) { + identifier = ButtonTopIdentifier; + } else if (indexPath.section == self.userButtonSection) { + identifier = ButtonBottomIdentifier; + } else { + identifier = ButtonDeleteIdentifier; + } + + cell = [tableView dequeueReusableCellWithIdentifier:identifier]; + + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; + + cell.textLabel.font = [UIFont systemFontOfSize:14]; + cell.textLabel.numberOfLines = 0; + cell.accessoryType = UITableViewCellAccessoryNone; + cell.selectionStyle = UITableViewCellSelectionStyleGray; + } + + // Set accessibilityTraits to UIAccessibilityTraitNone to make sure we're not setting the trait to an incorrect type for recycled cells. + cell.textLabel.accessibilityTraits = UIAccessibilityTraitNone; + + // button + NSString *titleString = nil; + + UIColor *titleColor = BIT_RGBCOLOR(35, 111, 251); + if ([self.view respondsToSelector:@selector(tintColor)]){ + titleColor = self.view.tintColor; + } + if (indexPath.section == 0) { + cell.textLabel.accessibilityTraits = UIAccessibilityTraitButton; + if ([strongManager numberOfMessages] == 0) { + titleString = BITHockeyLocalizedString(@"HockeyFeedbackListButtonWriteFeedback"); + } else { + titleString = BITHockeyLocalizedString(@"HockeyFeedbackListButtonWriteResponse"); + } + } else if (indexPath.section == self.userButtonSection) { + if ([strongManager requireUserName] == BITFeedbackUserDataElementRequired || + ([strongManager requireUserName] == BITFeedbackUserDataElementOptional && [strongManager userName] != nil) + ) { + cell.textLabel.accessibilityTraits = UIAccessibilityTraitStaticText; + titleString = [NSString stringWithFormat:BITHockeyLocalizedString(@"HockeyFeedbackListButtonUserDataWithName"), [strongManager userName] ?: @"-"]; + } else if ([strongManager requireUserEmail] == BITFeedbackUserDataElementRequired || + ([strongManager requireUserEmail] == BITFeedbackUserDataElementOptional && [strongManager userEmail] != nil) + ) { + cell.textLabel.accessibilityTraits = UIAccessibilityTraitStaticText; + titleString = [NSString stringWithFormat:BITHockeyLocalizedString(@"HockeyFeedbackListButtonUserDataWithEmail"), [strongManager userEmail] ?: @"-"]; + } else if ([strongManager requireUserName] == BITFeedbackUserDataElementOptional) { + cell.textLabel.accessibilityTraits = UIAccessibilityTraitButton; + titleString = BITHockeyLocalizedString(@"HockeyFeedbackListButtonUserDataSetName"); + } else { + cell.textLabel.accessibilityTraits = UIAccessibilityTraitButton; + titleString = BITHockeyLocalizedString(@"HockeyFeedbackListButtonUserDataSetEmail"); + } + } else { + cell.textLabel.accessibilityTraits = UIAccessibilityTraitButton; + titleString = BITHockeyLocalizedString(@"HockeyFeedbackListButtonDeleteAllMessages"); + titleColor = BIT_RGBCOLOR(251, 35, 35); + } + + cell.textLabel.text = titleString; + cell.textLabel.textColor = titleColor; + + return cell; + } else { + BITFeedbackListViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; + + if (!cell) { + cell = [[BITFeedbackListViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; + cell.accessoryType = UITableViewCellAccessoryNone; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + } + + if (indexPath.row == 0 || indexPath.row % 2 == 0) { + cell.backgroundStyle = BITFeedbackListViewCellBackgroundStyleAlternate; + } else { + cell.backgroundStyle = BITFeedbackListViewCellBackgroundStyleNormal; + } + + BITFeedbackMessage *message = [strongManager messageAtIndex:indexPath.row]; + cell.message = message; + cell.labelText.delegate = self; + cell.labelText.userInteractionEnabled = YES; + cell.delegate = self; + [cell setAttachments:message.previewableAttachments]; + + for (BITFeedbackMessageAttachment *attachment in message.attachments){ + if (attachment.needsLoadingFromURL && !attachment.isLoading){ + attachment.isLoading = YES; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:(NSURL *)[NSURL URLWithString:attachment.sourceURL]]; + __weak typeof (self) weakSelf = self; + NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + __block NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; + + NSURLSessionDataTask *task = [session dataTaskWithRequest:request + completionHandler: ^(NSData *data, NSURLResponse __unused *response, NSError *error) { + typeof (self) strongSelf = weakSelf; + + [session finishTasksAndInvalidate]; + + [strongSelf handleResponseForAttachment:attachment responseData:data error:error]; + }]; + [task resume]; + } + } + + if (indexPath.row != 0) { + UIView *lineView1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, cell.frame.size.width, 1)]; + lineView1.backgroundColor = BORDER_COLOR; + lineView1.autoresizingMask = UIViewAutoresizingFlexibleWidth; + [cell addSubview:lineView1]; + } + + return cell; + } +} + +- (void)handleResponseForAttachment:(BITFeedbackMessageAttachment *)attachment responseData:(NSData *)responseData error:(NSError *) __unused error { + attachment.isLoading = NO; + if (responseData.length) { + dispatch_async(dispatch_get_main_queue(), ^{ + [attachment replaceData:responseData]; + [[NSNotificationCenter defaultCenter] postNotificationName:kBITFeedbackUpdateAttachmentThumbnail object:attachment]; + [[BITHockeyManager sharedHockeyManager].feedbackManager saveMessages]; + [self.tableView reloadData]; + }); + } +} + + +- (BOOL)tableView:(UITableView *) __unused tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section == 1) + return YES; + + return NO; +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { + if (editingStyle == UITableViewCellEditingStyleDelete) { + BITFeedbackManager *strongManager = self.manager; + BITFeedbackMessage *message = [strongManager messageAtIndex:indexPath.row]; + BOOL messageHasAttachments = ([message attachments].count > 0); + + if ([strongManager deleteMessageAtIndex:indexPath.row]) { + if ([strongManager numberOfMessages] > 0) { + [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + } else { + [tableView reloadData]; + } + + if (messageHasAttachments) { + [self refreshPreviewItems]; + } + } + } +} + + +#pragma mark - Table view delegate + +- (CGFloat)tableView:(UITableView *) __unused tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section == 0 ) { + return 44; + } + if (indexPath.section >= 2) { + return 44; + } + + BITFeedbackMessage *message = [self.manager messageAtIndex:indexPath.row]; + if (!message) return 44; + + return [BITFeedbackListViewCell heightForRowWithMessage:message tableViewWidth:self.view.frame.size.width]; +} + +- (void)tableView:(UITableView *) __unused tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section == 0) { + [self newFeedbackAction:self]; + } else if (indexPath.section == self.userButtonSection) { + [self setUserDataAction:self]; + } else if (indexPath.section == self.deleteButtonSection) { + [self deleteAllMessagesAction:self]; + } +} + +#pragma mark - BITAttributedLabelDelegate + +- (void)attributedLabel:(BITAttributedLabel *) __unused label didSelectLinkWithURL:(NSURL *)url { + UIAlertControllerStyle controllerStyle = UIAlertControllerStyleAlert; + if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) { + controllerStyle = UIAlertControllerStyleActionSheet; + } + UIAlertController *linkAction = [UIAlertController alertControllerWithTitle:[url absoluteString] + message:nil + preferredStyle:controllerStyle]; + UIAlertAction* cancelAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackListLinkActionCancel") + style:UIAlertActionStyleCancel + handler:^(UIAlertAction __unused *action) {}]; + [linkAction addAction:cancelAction]; + UIAlertAction* openAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackListLinkActionOpen") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + [[UIApplication sharedApplication] openURL:(NSURL*)[NSURL URLWithString:(NSString*)[url absoluteString]]]; + }]; + [linkAction addAction:openAction]; + UIAlertAction* copyAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackListLinkActionCopy") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.URL = [NSURL URLWithString:(NSString*)[url absoluteString]]; + }]; + [linkAction addAction:copyAction]; + [self presentViewController:linkAction animated:YES completion:nil]; +} + +#pragma mark - UIActionSheetDelegate + +- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex { + if (buttonIndex == actionSheet.cancelButtonIndex) { + return; + } + + if ([actionSheet tag] == 0) { + if (buttonIndex == [actionSheet destructiveButtonIndex]) { + [self deleteAllMessages]; + } + } else { + if (buttonIndex == [actionSheet firstOtherButtonIndex]) { + [[UIApplication sharedApplication] openURL:(NSURL *)[NSURL URLWithString:actionSheet.title]]; + } else { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.URL = [NSURL URLWithString:actionSheet.title]; + } + } +} + +#pragma mark - ListViewCellDelegate + +- (void)listCell:(id) __unused cell didSelectAttachment:(BITFeedbackMessageAttachment *)attachment { + if (!self.cachedPreviewItems){ + [self refreshPreviewItems]; + } + + QLPreviewController *previewController = [[QLPreviewController alloc] init]; + previewController.dataSource = self; + + [self presentViewController:previewController animated:YES completion:nil]; + + if (self.cachedPreviewItems.count > [self.cachedPreviewItems indexOfObject:attachment]) { + [previewController setCurrentPreviewItemIndex:[self.cachedPreviewItems indexOfObject:attachment]]; + } +} + +- (void)refreshPreviewItems { + self.cachedPreviewItems = nil; + NSMutableArray *collectedAttachments = [NSMutableArray new]; + BITFeedbackManager *strongManager = self.manager; + for (uint i = 0; i < strongManager.numberOfMessages; i++) { + BITFeedbackMessage *message = [strongManager messageAtIndex:i]; + [collectedAttachments addObjectsFromArray:message.previewableAttachments]; + } + + self.cachedPreviewItems = collectedAttachments; +} + +- (NSInteger)numberOfPreviewItemsInPreviewController:(QLPreviewController *) __unused controller { + return self.cachedPreviewItems.count; +} + +- (id )previewController:(QLPreviewController *)controller previewItemAtIndex:(NSInteger)index { + if (index >= 0) { + __weak QLPreviewController* blockController = controller; + BITFeedbackMessageAttachment *attachment = self.cachedPreviewItems[index]; + + if (attachment.needsLoadingFromURL && !attachment.isLoading) { + attachment.isLoading = YES; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:(NSURL *)[NSURL URLWithString:attachment.sourceURL]]; + + __weak typeof (self) weakSelf = self; + NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + __block NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; + + NSURLSessionDataTask *task = [session dataTaskWithRequest:request + completionHandler: ^(NSData *data, NSURLResponse __unused *response, NSError __unused *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + typeof (self) strongSelf = weakSelf; + + [session finishTasksAndInvalidate]; + + [strongSelf previewController:blockController updateAttachment:attachment data:data]; + }); + }]; + [task resume]; + return attachment; + } else { + return self.cachedPreviewItems[index]; + } + } + + return [self placeholder]; +} + +- (void)previewController:(QLPreviewController *)controller updateAttachment:(BITFeedbackMessageAttachment *)attachment data:( NSData *)data { + attachment.isLoading = NO; + if (data.length) { + [attachment replaceData:data]; + [controller reloadData]; + + [[BITHockeyManager sharedHockeyManager].feedbackManager saveMessages]; + } else { + [controller reloadData]; + } +} + +- (BITFeedbackMessageAttachment *)placeholder { + UIImage *placeholderImage = bit_imageNamed(@"FeedbackPlaceHolder", BITHOCKEYSDK_BUNDLE); + + BITFeedbackMessageAttachment *placeholder = [BITFeedbackMessageAttachment attachmentWithData:UIImageJPEGRepresentation(placeholderImage, (CGFloat)0.7) contentType:@"image/jpeg"]; + + return placeholder; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackManager.h b/submodules/HockeySDK-iOS/Classes/BITFeedbackManager.h new file mode 100644 index 0000000000..507788df71 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackManager.h @@ -0,0 +1,365 @@ +/* + * 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 + +#import "BITHockeyBaseManager.h" +#import "BITFeedbackListViewController.h" +#import "BITFeedbackComposeViewController.h" + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +// Notification message which tells that loading messages finished +#define BITHockeyFeedbackMessagesLoadingStarted @"BITHockeyFeedbackMessagesLoadingStarted" + +// Notification message which tells that loading messages finished +#define BITHockeyFeedbackMessagesLoadingFinished @"BITHockeyFeedbackMessagesLoadingFinished" + + +/** + * Defines behavior of the user data field + */ +typedef NS_ENUM(NSInteger, BITFeedbackUserDataElement) { + /** + * don't ask for this user data element at all + */ + BITFeedbackUserDataElementDontShow = 0, + /** + * the user may provide it, but does not have to + */ + BITFeedbackUserDataElementOptional = 1, + /** + * the user has to provide this to continue + */ + BITFeedbackUserDataElementRequired = 2 +}; + +/** + * Available modes for opening the feedback compose interface with a screenshot attached + */ +typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { + /** + * No SDK provided trigger is active. + */ + BITFeedbackObservationNone = 0, + /** + * Triggers when the user takes a screenshot. This will grab the latest image from the camera roll. Requires iOS 7 or later! + */ + BITFeedbackObservationModeOnScreenshot = 1, + /** + * Triggers when the user taps with three fingers on the screen. Captures a screenshot and attaches it to the composer. + */ + BITFeedbackObservationModeThreeFingerTap = 2, + /** + * Allows both BITFeedbackObservationModeOnScreenshot and BITFeedbackObservationModeThreeFingerTap at the same time. + */ + BITFeedbackObservationModeAll = 3 +}; + + +@class BITFeedbackMessage; +@protocol BITFeedbackManagerDelegate; + +/** + The feedback module. + + This is the HockeySDK module for letting your users communicate directly with you via + the app and an integrated user interface. It provides a single threaded + discussion with a user running your app. + + You should never create your own instance of `BITFeedbackManager` but use the one provided + by the `[BITHockeyManager sharedHockeyManager]`: + + [BITHockeyManager sharedHockeyManager].feedbackManager + + The user interface provides a list view that can be presented modally using + `[BITFeedbackManager showFeedbackListView]` or adding + `[BITFeedbackManager feedbackListViewController:]` to push onto a navigation stack. + This list integrates all features to load new messages, write new messages, view messages + and ask the user for additional (optional) data like name and email. + + If the user provides the email address, all responses from the server will also be sent + to the user via email and the user is also able to respond directly via email, too. + + The message list interface also contains options to locally delete single messages + by swiping over them, or deleting all messages. This will not delete the messages + on the server, though! + + It also integrates actions to invoke the user interface to compose a new message, + reload the list content from the server and change the users name or email if these + are allowed to be set. + + It is also possible to invoke the user interface to compose a new message in your + own code, by calling `[BITFeedbackManager showFeedbackComposeView]` modally or adding + `[BITFeedbackManager feedbackComposeViewController]` to push onto a navigation stack. + + If new messages are written while the device is offline, the SDK automatically retries to + send them once the app starts again or gets active again, or if the notification + `BITHockeyNetworkDidBecomeReachableNotification` is fired. + + A third option is to include the `BITFeedbackActivity` into an UIActivityViewController. + This can be useful if you present some data that users can not only share but also + report back to the developer because they have some problems, e.g. webcams not working + any more. The activity provides a default title and image that can also be customized. + + New messages are automatically loaded on startup, when the app becomes active again + or when the notification `BITHockeyNetworkDidBecomeReachableNotification` is fired. This + only happens if the user ever did initiate a conversation by writing the first + feedback message. The app developer has to fire this notification to trigger another retry + when it detects the device having network access again. The SDK only retries automatically + when the app becomes active again. + + Implementing the `BITFeedbackManagerDelegate` protocol will notify your app when a new + message was received from the server. The `BITFeedbackComposeViewControllerDelegate` + protocol informs your app about events related to sending feedback messages. + + */ + +@interface BITFeedbackManager : BITHockeyBaseManager + +///----------------------------------------------------------------------------- +/// @name General settings +///----------------------------------------------------------------------------- + + +/** + Define if a name has to be provided by the user when providing feedback + + - `BITFeedbackUserDataElementDontShow`: Don't ask for this user data element at all + - `BITFeedbackUserDataElementOptional`: The user may provide it, but does not have to + - `BITFeedbackUserDataElementRequired`: The user has to provide this to continue + + The default value is `BITFeedbackUserDataElementOptional`. + + @warning If you provide a non nil value for the `BITFeedbackManager` class via + `[BITHockeyManagerDelegate userNameForHockeyManager:componentManager:]` then this + property will automatically be set to `BITFeedbackUserDataElementDontShow` + + @see BITFeedbackUserDataElement + @see requireUserEmail + @see `[BITHockeyManagerDelegate userNameForHockeyManager:componentManager:]` + */ +@property (nonatomic, readwrite) BITFeedbackUserDataElement requireUserName; + + +/** + Define if an email address has to be provided by the user when providing feedback + + If the user provides the email address, all responses from the server will also be send + to the user via email and the user is also able to respond directly via email too. + + - `BITFeedbackUserDataElementDontShow`: Don't ask for this user data element at all + - `BITFeedbackUserDataElementOptional`: The user may provide it, but does not have to + - `BITFeedbackUserDataElementRequired`: The user has to provide this to continue + + The default value is `BITFeedbackUserDataElementOptional`. + + @warning If you provide a non nil value for the `BITFeedbackManager` class via + `[BITHockeyManagerDelegate userEmailForHockeyManager:componentManager:]` then this + property will automatically be set to `BITFeedbackUserDataElementDontShow` + + @see BITFeedbackUserDataElement + @see requireUserName + @see `[BITHockeyManagerDelegate userEmailForHockeyManager:componentManager:]` + */ +@property (nonatomic, readwrite) BITFeedbackUserDataElement requireUserEmail; + + +/** + Indicates if an alert should be shown when new messages have arrived + + This lets the user view the new feedback by choosing the appropriate option + in the alert sheet, and the `BITFeedbackListViewController` will be shown. + + The alert is only shown, if the newest message didn't originate from the current user. + This requires the users email address to be present! The optional userid property + cannot be used, because users could also answer via email and then this information + is not available. + + Default is `YES` + @see feedbackListViewController: + @see requireUserEmail + @see `[BITHockeyManagerDelegate userEmailForHockeyManager:componentManager:]` + */ +@property (nonatomic, readwrite) BOOL showAlertOnIncomingMessages; + + +/** + Define the trigger that opens the feedback composer and attaches a screenshot + + The following modes are available: + + - `BITFeedbackObservationNone`: No SDK based trigger is active. You can implement your + own trigger and then call `[[BITHockeyManager sharedHockeyManager].feedbackManager showFeedbackComposeViewWithGeneratedScreenshot];` to handle your custom events + that should trigger this. + - `BITFeedbackObservationModeOnScreenshot`: Triggers when the user takes a screenshot. + This will grab the latest image from the camera roll. It also requires to add a NSPhotoLibraryUsageDescription to your app's Info.plist. + - `BITFeedbackObservationModeThreeFingerTap`: Triggers when the user taps on the screen with three fingers. Takes a screenshot and attaches it to the composer. It also requires to add a NSPhotoLibraryUsageDescription to your app's Info.plist. + + Default is `BITFeedbackObservationNone`. + If BITFeedbackManger was disabled, setting a new value will be ignored. + @see `[BITHockeyManager disableFeedbackManager]` + + @see showFeedbackComposeViewWithGeneratedScreenshot + */ +@property (nonatomic, readwrite) BITFeedbackObservationMode feedbackObservationMode; + +/** + Don't show the option to add images from the photo library + + This is helpful if your application is landscape only, since the system UI for + selecting an image from the photo library is portrait only + + This setting is used for all feedback compose views that are created by the + `BITFeedbackManager`. If you invoke your own `BITFeedbackComposeViewController`, + then set the appropriate property on the view controller directl!. + */ +@property (nonatomic) BOOL feedbackComposeHideImageAttachmentButton; + + +///----------------------------------------------------------------------------- +/// @name User Interface +///----------------------------------------------------------------------------- + + +/** + Indicates if a forced user data UI presentation is shown modal + + If `requireUserName` and/or `requireUserEmail` are enabled, the first presentation + of `feedbackListViewController:` and subsequent `feedbackComposeViewController:` + will automatically present a UI that lets the user provide this data and compose + a message. By default this is shown (since SDK 3.1) as a modal sheet. + + If you want the SDK to push this UI onto the navigation stack in this specific scenario, + then change the property to `NO`. + + @warning If you are presenting the `BITFeedbackListViewController` in a popover, this property should not be changed! + + Default is `YES` + @see requireUserName + @see requireUserEmail + @see showFeedbackComposeView + @see feedbackComposeViewController + @see showFeedbackListView + @see feedbackListViewController: + */ +@property (nonatomic, readwrite) BOOL showFirstRequiredPresentationModal; + + +/** + Return a screenshot UIImage instance from the current visible screen + + @return UIImage instance containing a screenshot of the current screen + */ +- (UIImage *)screenshot; + + +/** + Present the modal feedback list user interface. + + @warning This methods needs to be called on the main thread! + */ +- (void)showFeedbackListView; + + +/** + Create an feedback list view + + @param modal Return a view ready for modal presentation with integrated navigation bar + @return `BITFeedbackListViewController` The feedback list view controller, + e.g. to push it onto a navigation stack. + */ +- (BITFeedbackListViewController *)feedbackListViewController:(BOOL)modal; + + +/** + Present the modal feedback compose message user interface. + + @warning This methods needs to be called on the main thread! + */ +- (void)showFeedbackComposeView; + +/** + Present the modal feedback compose message user interface with the items given. + + All NSString-Content in the array will be concatenated and result in the message, + while all UIImage and NSData-instances will be turned into attachments. + + Alternatively you can implement the `preparedItemsForFeedbackManager:` delegate method + and call `showFeedbackComposeView` instead. If you use both, the items from the delegate method + and the items passed with this method will be combined. + + @param items an NSArray with objects that should be attached + @see `[BITFeedbackComposeViewController prepareWithItems:]` + @see `BITFeedbackManagerDelegate preparedItemsForFeedbackManager:` + @warning This methods needs to be called on the main thread! + */ +- (void)showFeedbackComposeViewWithPreparedItems:(nullable NSArray *)items; + +/** + Presents a modal feedback compose interface with a screenshot attached which is taken at the time of calling this method. + + This should be used when your own trigger fires. The following code should be used: + + [[BITHockeyManager sharedHockeyManager].feedbackManager showFeedbackComposeViewWithGeneratedScreenshot]; + + @see feedbackObservationMode + @warning This methods needs to be called on the main thread! + */ +- (void)showFeedbackComposeViewWithGeneratedScreenshot; + + +/** + Create a feedback compose view + + This method also adds items from `feedbackComposerPreparedItems` and + the `preparedItemsForFeedbackManager:` delegate methods to the instance of + `BITFeedbackComposeViewController` that will be returned. + + Example to show a modal feedback compose UI with prefilled text + + BITFeedbackComposeViewController *feedbackCompose = [[BITHockeyManager sharedHockeyManager].feedbackManager feedbackComposeViewController]; + + [feedbackCompose prepareWithItems: + @[@"Adding some example default text and also adding a link.", + [NSURL URLWithString:@"http://hockeayyp.net/"]]]; + + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:feedbackCompose]; + navController.modalPresentationStyle = UIModalPresentationFormSheet; + [self presentViewController:navController animated:YES completion:nil]; + + @return `BITFeedbackComposeViewController` The compose feedback view controller, + e.g. to push it onto a navigation stack. + */ +- (BITFeedbackComposeViewController *)feedbackComposeViewController; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackManager.m b/submodules/HockeySDK-iOS/Classes/BITFeedbackManager.m new file mode 100644 index 0000000000..04f7d16bd6 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackManager.m @@ -0,0 +1,1302 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2012-2016 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import + +#import "HockeySDKPrivate.h" + +#import "BITFeedbackManager.h" +#import "BITFeedbackMessageAttachment.h" +#import "BITFeedbackManagerPrivate.h" +#import "BITHockeyBaseManagerPrivate.h" + +#import "HockeySDKNullability.h" +#import "BITHockeyHelper.h" +#import "BITHockeyHelper+Application.h" +#import "BITHockeyAppClient.h" + +#define kBITFeedbackUserDataAsked @"HockeyFeedbackUserDataAsked" +#define kBITFeedbackDateOfLastCheck @"HockeyFeedbackDateOfLastCheck" +#define kBITFeedbackMessages @"HockeyFeedbackMessages" +#define kBITFeedbackToken @"HockeyFeedbackToken" +#define kBITFeedbackUserID @"HockeyFeedbackuserID" +#define kBITFeedbackName @"HockeyFeedbackName" +#define kBITFeedbackEmail @"HockeyFeedbackEmail" +#define kBITFeedbackLastMessageID @"HockeyFeedbackLastMessageID" +#define kBITFeedbackAppID @"HockeyFeedbackAppID" + +NSString *const kBITFeedbackUpdateAttachmentThumbnail = @"BITFeedbackUpdateAttachmentThumbnail"; + +typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage); + +@interface BITFeedbackManager () + +@property (nonatomic, strong) NSFileManager *fileManager; +@property (nonatomic, copy) NSString *settingsFile; +@property (nonatomic, weak) id appDidBecomeActiveObserver; +@property (nonatomic, weak) id appDidEnterBackgroundObserver; +@property (nonatomic, weak) id networkDidBecomeReachableObserver; +@property (nonatomic) BOOL incomingMessagesAlertShowing; +@property (nonatomic) BOOL didEnterBackgroundState; +@property (nonatomic) BOOL networkRequestInProgress; +@property (nonatomic) BITFeedbackObservationMode observationMode; + +@end + +@implementation BITFeedbackManager + +#pragma mark - Initialization + +- (instancetype)init { + if ((self = [super init])) { + _currentFeedbackListViewController = nil; + _currentFeedbackComposeViewController = nil; + _didAskUserData = NO; + + _requireUserName = BITFeedbackUserDataElementOptional; + _requireUserEmail = BITFeedbackUserDataElementOptional; + _showAlertOnIncomingMessages = YES; + _showFirstRequiredPresentationModal = YES; + + _disableFeedbackManager = NO; + _networkRequestInProgress = NO; + _incomingMessagesAlertShowing = NO; + _lastCheck = nil; + _token = nil; + _lastMessageID = nil; + + self.feedbackList = [NSMutableArray array]; + + _fileManager = [[NSFileManager alloc] init]; + + _settingsFile = [bit_settingsDir() stringByAppendingPathComponent:BITHOCKEY_FEEDBACK_SETTINGS]; + + _userID = nil; + _userName = nil; + _userEmail = nil; + } + return self; +} + +- (void)dealloc { + [self unregisterObservers]; +} + +- (void)didBecomeActiveActions { + if ([self isFeedbackManagerDisabled]) return; + if (!self.didEnterBackgroundState) return; + + self.didEnterBackgroundState = NO; + + if ([self.feedbackList count] == 0) { + [self loadMessages]; + } else { + [self updateAppDefinedUserData]; + } + + if ([self allowFetchingNewMessages]) { + [self updateMessagesList]; + } +} + +- (void)didEnterBackgroundActions { + self.didEnterBackgroundState = NO; + + if ([BITHockeyHelper applicationState] == BITApplicationStateBackground) { + self.didEnterBackgroundState = YES; + } +} + +#pragma mark - Observers + +- (void)registerObservers { + __weak typeof(self) weakSelf = self; + if (nil == self.appDidEnterBackgroundObserver) { + self.appDidEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf didEnterBackgroundActions]; + }]; + } + if (nil == self.appDidBecomeActiveObserver) { + self.appDidBecomeActiveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf didBecomeActiveActions]; + }]; + } + if (nil == self.networkDidBecomeReachableObserver) { + self.networkDidBecomeReachableObserver = [[NSNotificationCenter defaultCenter] addObserverForName:BITHockeyNetworkDidBecomeReachableNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf didBecomeActiveActions]; + }]; + } +} + +- (void)unregisterObservers { + id strongDidEnterBackgroundObserver = self.appDidEnterBackgroundObserver; + id strongDidBecomeActiveObserver = self.appDidBecomeActiveObserver; + id strongNetworkDidBecomeReachableObserver = self.networkDidBecomeReachableObserver; + if (strongDidEnterBackgroundObserver) { + [[NSNotificationCenter defaultCenter] removeObserver:strongDidEnterBackgroundObserver]; + self.appDidEnterBackgroundObserver = nil; + } + if (strongDidBecomeActiveObserver) { + [[NSNotificationCenter defaultCenter] removeObserver:strongDidBecomeActiveObserver]; + self.appDidBecomeActiveObserver = nil; + } + if (strongNetworkDidBecomeReachableObserver) { + [[NSNotificationCenter defaultCenter] removeObserver:strongNetworkDidBecomeReachableObserver]; + self.networkDidBecomeReachableObserver = nil; + } +} + +#pragma mark - Private methods + +- (NSString *)uuidString { + CFUUIDRef theToken = CFUUIDCreate(NULL); + NSString *stringUUID = (__bridge_transfer NSString *) CFUUIDCreateString(NULL, theToken); + CFRelease(theToken); + + return stringUUID; +} + +- (NSString *)uuidAsLowerCaseAndShortened { + return [[[self uuidString] lowercaseString] stringByReplacingOccurrencesOfString:@"-" withString:@""]; +} + +#pragma mark - Feedback Modal UI + +- (UIImage *)screenshot { + return bit_screenshot(); +} + +- (BITFeedbackListViewController *)feedbackListViewController:(BOOL)modal { + return [[BITFeedbackListViewController alloc] initWithStyle:UITableViewStyleGrouped modal:modal]; +} + +- (void)showFeedbackListView { + if (self.currentFeedbackListViewController) { + BITHockeyLogDebug(@"INFO: update view already visible, aborting"); + return; + } + dispatch_async(dispatch_get_main_queue(), ^{ + [self showView:[self feedbackListViewController:YES]]; + }); +} + + +- (BITFeedbackComposeViewController *)feedbackComposeViewController { + BITFeedbackComposeViewController *composeViewController = [[BITFeedbackComposeViewController alloc] init]; + + NSArray *preparedItems = [NSArray array]; + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(preparedItemsForFeedbackManager:)]) { + preparedItems = [preparedItems arrayByAddingObjectsFromArray:(NSArray *)[strongDelegate preparedItemsForFeedbackManager:self]]; + } + + [composeViewController prepareWithItems:preparedItems]; + [composeViewController setHideImageAttachmentButton:self.feedbackComposeHideImageAttachmentButton]; + + // by default set the delegate to be identical to the one of BITFeedbackManager + [composeViewController setDelegate:strongDelegate]; + return composeViewController; +} + +- (void)showFeedbackComposeView { + [self showFeedbackComposeViewWithPreparedItems:nil]; +} + +- (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items { + if (self.currentFeedbackComposeViewController) { + BITHockeyLogDebug(@"INFO: Feedback view already visible, aborting"); + return; + } + BITFeedbackComposeViewController *composeView = [self feedbackComposeViewController]; + [composeView prepareWithItems:items]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self showView:composeView]; + }); +} + +- (void)showFeedbackComposeViewWithGeneratedScreenshot { + UIImage *screenshot = bit_screenshot(); + [self showFeedbackComposeViewWithPreparedItems:@[screenshot]]; +} + +#pragma mark - Manager Control + +- (void)startManager { + if ([self isFeedbackManagerDisabled]) return; + + [self registerObservers]; + + [self isiOS10PhotoPolicySet]; + + // we are already delayed, so the notification already came in and this won't invoked twice + switch ([BITHockeyHelper applicationState]) { + case BITApplicationStateActive: + // we did startup, so yes we are coming from background + self.didEnterBackgroundState = YES; + + [self didBecomeActiveActions]; + break; + case BITApplicationStateBackground: + case BITApplicationStateInactive: + case BITApplicationStateUnknown: + // do nothing, wait for active state + break; + } +} + +- (BOOL)allowFetchingNewMessages { + BOOL fetchNewMessages = YES; + id strongDelegate = [BITHockeyManager sharedHockeyManager].delegate; + if ([strongDelegate respondsToSelector:@selector(allowAutomaticFetchingForNewFeedbackForManager:)]) { + fetchNewMessages = [strongDelegate allowAutomaticFetchingForNewFeedbackForManager:self]; + } + return fetchNewMessages; +} + +- (void)updateMessagesList { + if (self.networkRequestInProgress) return; + + NSArray *pendingMessages = [self messagesWithStatus:BITFeedbackMessageStatusSendPending]; + if ([pendingMessages count] > 0) { + [self submitPendingMessages]; + } else { + [self fetchMessageUpdates]; + } +} + +- (void)updateMessagesListIfRequired { + double now = [[NSDate date] timeIntervalSince1970]; + if ((now - [self.lastCheck timeIntervalSince1970] > 30)) { + [self updateMessagesList]; + } +} + +- (BOOL)updateUserIDUsingKeychainAndDelegate { + BOOL availableViaDelegate = NO; + + NSString *userID = [self stringValueFromKeychainForKey:kBITHockeyMetaUserID]; + id strongDelegate = [BITHockeyManager sharedHockeyManager].delegate; + if ([strongDelegate respondsToSelector:@selector(userIDForHockeyManager:componentManager:)]) { + userID = [strongDelegate userIDForHockeyManager:[BITHockeyManager sharedHockeyManager] componentManager:self]; + } + + if (userID) { + availableViaDelegate = YES; + self.userID = userID; + } + + return availableViaDelegate; +} + +- (BOOL)updateUserNameUsingKeychainAndDelegate { + BOOL availableViaDelegate = NO; + + NSString *userName = [self stringValueFromKeychainForKey:kBITHockeyMetaUserName]; + id strongDelegate = [BITHockeyManager sharedHockeyManager].delegate; + if ([strongDelegate respondsToSelector:@selector(userNameForHockeyManager:componentManager:)]) { + userName = [strongDelegate userNameForHockeyManager:[BITHockeyManager sharedHockeyManager] componentManager:self]; + } + + if (userName) { + availableViaDelegate = YES; + self.userName = userName; + self.requireUserName = BITFeedbackUserDataElementDontShow; + } + + return availableViaDelegate; +} + +- (BOOL)updateUserEmailUsingKeychainAndDelegate { + BOOL availableViaDelegate = NO; + + NSString *userEmail = [self stringValueFromKeychainForKey:kBITHockeyMetaUserEmail]; + id strongDelegate = [BITHockeyManager sharedHockeyManager].delegate; + if ([strongDelegate respondsToSelector:@selector(userEmailForHockeyManager:componentManager:)]) { + userEmail = [strongDelegate userEmailForHockeyManager:[BITHockeyManager sharedHockeyManager] componentManager:self]; + } + + if (userEmail) { + availableViaDelegate = YES; + self.userEmail = userEmail; + self.requireUserEmail = BITFeedbackUserDataElementDontShow; + } + + return availableViaDelegate; +} + +- (void)updateAppDefinedUserData { + [self updateUserIDUsingKeychainAndDelegate]; + [self updateUserNameUsingKeychainAndDelegate]; + [self updateUserEmailUsingKeychainAndDelegate]; + + // if both values are shown via the delegates, we never ever did ask and will never ever ask for user data + if (self.requireUserName == BITFeedbackUserDataElementDontShow && + self.requireUserEmail == BITFeedbackUserDataElementDontShow) { + self.didAskUserData = NO; + } +} + +- (BOOL)isiOS10PhotoPolicySet { + BOOL isiOS10PhotoPolicySet = [BITHockeyHelper isPhotoAccessPossible]; + if (bit_isDebuggerAttached()) { + if (!isiOS10PhotoPolicySet) { + BITHockeyLogWarning(@"You are using HockeyApp's Feedback feature in iOS 10 or later. iOS 10 requires you to add the usage strings to your app's info.plist. " + @"Attaching screenshots to feedback is disabled. Please add the String for NSPhotoLibraryUsageDescription to your info.plist to enable screenshot attachments."); + } + } + return isiOS10PhotoPolicySet; +} + +#pragma mark - Local Storage + +- (void)loadMessages { + BOOL userIDViaDelegate = [self updateUserIDUsingKeychainAndDelegate]; + BOOL userNameViaDelegate = [self updateUserNameUsingKeychainAndDelegate]; + BOOL userEmailViaDelegate = [self updateUserEmailUsingKeychainAndDelegate]; + + if (![self.fileManager fileExistsAtPath:self.settingsFile]) + return; + + NSData *codedData = [[NSData alloc] initWithContentsOfFile:self.settingsFile]; + if (codedData == nil) return; + + NSKeyedUnarchiver *unarchiver = nil; + + @try { + unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:codedData]; + } + @catch (NSException __unused *exception) { + return; + } + + if (!userIDViaDelegate) { + if ([unarchiver containsValueForKey:kBITFeedbackUserID]) { + self.userID = [unarchiver decodeObjectForKey:kBITFeedbackUserID]; + [self addStringValueToKeychain:self.userID forKey:kBITFeedbackUserID]; + } + self.userID = [self stringValueFromKeychainForKey:kBITFeedbackUserID]; + } + + if (!userNameViaDelegate) { + if ([unarchiver containsValueForKey:kBITFeedbackName]) { + self.userName = [unarchiver decodeObjectForKey:kBITFeedbackName]; + [self addStringValueToKeychain:self.userName forKey:kBITFeedbackName]; + } + self.userName = [self stringValueFromKeychainForKey:kBITFeedbackName]; + } + + if (!userEmailViaDelegate) { + if ([unarchiver containsValueForKey:kBITFeedbackEmail]) { + self.userEmail = [unarchiver decodeObjectForKey:kBITFeedbackEmail]; + [self addStringValueToKeychain:self.userEmail forKey:kBITFeedbackEmail]; + } + self.userEmail = [self stringValueFromKeychainForKey:kBITFeedbackEmail]; + } + + if ([unarchiver containsValueForKey:kBITFeedbackUserDataAsked]) + self.didAskUserData = YES; + + if ([unarchiver containsValueForKey:kBITFeedbackToken]) { + self.token = [unarchiver decodeObjectForKey:kBITFeedbackToken]; + [self addStringValueToKeychain:self.token forKey:kBITFeedbackToken]; + } + self.token = [self stringValueFromKeychainForKey:kBITFeedbackToken]; + + if ([unarchiver containsValueForKey:kBITFeedbackAppID]) { + NSString *appID = [unarchiver decodeObjectForKey:kBITFeedbackAppID]; + + // the stored thread is from another application identifier, so clear the token + // which will cause the new posts to create a new thread on the server for the + // current app identifier + if ([appID compare:self.appIdentifier] != NSOrderedSame) { + self.token = nil; + } + } + + if ([self shouldForceNewThread]) { + self.token = nil; + } + + if ([unarchiver containsValueForKey:kBITFeedbackDateOfLastCheck]) + self.lastCheck = [unarchiver decodeObjectForKey:kBITFeedbackDateOfLastCheck]; + + if ([unarchiver containsValueForKey:kBITFeedbackLastMessageID]) + self.lastMessageID = [unarchiver decodeObjectForKey:kBITFeedbackLastMessageID]; + + if ([unarchiver containsValueForKey:kBITFeedbackMessages]) { + [self.feedbackList setArray:(NSArray *)[unarchiver decodeObjectForKey:kBITFeedbackMessages]]; + + [self sortFeedbackList]; + + // inform the UI to update its data in case the list is already showing + [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingFinished object:nil]; + } + + [unarchiver finishDecoding]; + + if (!self.lastCheck) { + self.lastCheck = [NSDate distantPast]; + } +} + + +- (void)saveMessages { + [self sortFeedbackList]; + + NSMutableData *data = [[NSMutableData alloc] init]; + NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; + + if (self.didAskUserData) + [archiver encodeObject:[NSNumber numberWithBool:YES] forKey:kBITFeedbackUserDataAsked]; + + if (self.token) + [self addStringValueToKeychain:self.token forKey:kBITFeedbackToken]; + + if (self.appIdentifier) + [archiver encodeObject:self.appIdentifier forKey:kBITFeedbackAppID]; + + if (self.userID) + [self addStringValueToKeychain:self.userID forKey:kBITFeedbackUserID]; + + if (self.userName) + [self addStringValueToKeychain:self.userName forKey:kBITFeedbackName]; + + if (self.userEmail) + [self addStringValueToKeychain:self.userEmail forKey:kBITFeedbackEmail]; + + if (self.lastCheck) + [archiver encodeObject:self.lastCheck forKey:kBITFeedbackDateOfLastCheck]; + + if (self.lastMessageID) + [archiver encodeObject:self.lastMessageID forKey:kBITFeedbackLastMessageID]; + + [archiver encodeObject:self.feedbackList forKey:kBITFeedbackMessages]; + + [archiver finishEncoding]; + [data writeToFile:self.settingsFile atomically:YES]; +} + + +- (void)updateDidAskUserData { + if (!self.didAskUserData) { + self.didAskUserData = YES; + + [self saveMessages]; + } +} + +#pragma mark - Messages + +- (void)sortFeedbackList { + [self.feedbackList sortUsingComparator:^(BITFeedbackMessage *obj1, BITFeedbackMessage *obj2) { + NSDate *date1 = [obj1 date]; + NSDate *date2 = [obj2 date]; + + // not send, in conflict and send in progress messages on top, sorted by date + // read and unread on bottom, sorted by date + // archived on the very bottom + + if ([obj1 status] >= BITFeedbackMessageStatusSendInProgress && [obj2 status] < BITFeedbackMessageStatusSendInProgress) { + return NSOrderedDescending; + } else if ([obj1 status] < BITFeedbackMessageStatusSendInProgress && [obj2 status] >= BITFeedbackMessageStatusSendInProgress) { + return NSOrderedAscending; + } else if ([obj1 status] == BITFeedbackMessageStatusArchived && [obj2 status] < BITFeedbackMessageStatusArchived) { + return NSOrderedDescending; + } else if ([obj1 status] < BITFeedbackMessageStatusArchived && [obj2 status] == BITFeedbackMessageStatusArchived) { + return NSOrderedAscending; + } else { + return (NSInteger) [date2 compare:date1]; + } + }]; +} + +- (NSUInteger)numberOfMessages { + return [self.feedbackList count]; +} + +- (BITFeedbackMessage *)messageAtIndex:(NSUInteger)index { + if ([self.feedbackList count] > index) { + return [self.feedbackList objectAtIndex:index]; + } + + return nil; +} + +- (BITFeedbackMessage *)messageWithID:(NSNumber *)messageID { + __block BITFeedbackMessage *message = nil; + + [self.feedbackList enumerateObjectsUsingBlock:^(BITFeedbackMessage *objMessage, NSUInteger __unused messagesIdx, BOOL *stop) { + if ([[objMessage identifier] isEqualToNumber:messageID]) { + message = objMessage; + *stop = YES; + } + }]; + + return message; +} + +- (NSArray *)messagesWithStatus:(BITFeedbackMessageStatus)status { + NSMutableArray *resultMessages = [[NSMutableArray alloc] initWithCapacity:[self.feedbackList count]]; + + [self.feedbackList enumerateObjectsUsingBlock:^(BITFeedbackMessage *objMessage, NSUInteger __unused messagesIdx, BOOL __unused *stop) { + if ([objMessage status] == status) { + [resultMessages addObject:objMessage]; + } + }]; + + return [NSArray arrayWithArray:resultMessages];; +} + +- (BITFeedbackMessage *)lastMessageHavingID { + __block BITFeedbackMessage *message = nil; + + + // Note: the logic here is slightly different than in our mac SDK, as self.feedbackList is sorted in different order. + // Compare the implementation of - (void)sortFeedbackList; in both SDKs. + [self.feedbackList enumerateObjectsUsingBlock:^(BITFeedbackMessage *objMessage, NSUInteger __unused messagesIdx, BOOL *stop) { + if ([[objMessage identifier] integerValue] != 0) { + message = objMessage; + *stop = YES; + } + }]; + + return message; +} + +- (void)markSendInProgressMessagesAsPending { + // make sure message that may have not been send successfully, get back into the right state to be send again + [self.feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger __unused messagesIdx, BOOL __unused *stop) { + if ([(BITFeedbackMessage *) objMessage status] == BITFeedbackMessageStatusSendInProgress) + [(BITFeedbackMessage *) objMessage setStatus:BITFeedbackMessageStatusSendPending]; + }]; +} + +- (void)markSendInProgressMessagesAsInConflict { + // make sure message that may have not been send successfully, get back into the right state to be send again + [self.feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger __unused messagesIdx, BOOL __unused *stop) { + if ([(BITFeedbackMessage *) objMessage status] == BITFeedbackMessageStatusSendInProgress) + [(BITFeedbackMessage *) objMessage setStatus:BITFeedbackMessageStatusInConflict]; + }]; +} + +- (void)updateLastMessageID { + BITFeedbackMessage *lastMessageHavingID = [self lastMessageHavingID]; + if (lastMessageHavingID) { + if (!self.lastMessageID || [self.lastMessageID compare:[lastMessageHavingID identifier]] != NSOrderedSame) + self.lastMessageID = [lastMessageHavingID identifier]; + } +} + +- (BOOL)deleteMessageAtIndex:(NSUInteger)index { + if (self.feedbackList && [self.feedbackList count] > index && [self.feedbackList objectAtIndex:index]) { + BITFeedbackMessage *message = self.feedbackList[index]; + [message deleteContents]; + [self.feedbackList removeObjectAtIndex:index]; + + [self saveMessages]; + return YES; + } + + return NO; +} + +- (void)deleteAllMessages { + [self.feedbackList removeAllObjects]; + + [self saveMessages]; +} + +- (BOOL)shouldForceNewThread { + id strongDelegate = self.delegate; + if (strongDelegate && [strongDelegate respondsToSelector:@selector(forceNewFeedbackThreadForFeedbackManager:)]) { + return [strongDelegate forceNewFeedbackThreadForFeedbackManager:self]; + } else { + return NO; + } +} + + +#pragma mark - User + +- (BOOL)askManualUserDataAvailable { + [self updateAppDefinedUserData]; + + if (self.requireUserName == BITFeedbackUserDataElementDontShow && + self.requireUserEmail == BITFeedbackUserDataElementDontShow) + return NO; + + return YES; +} + +- (BOOL)requireManualUserDataMissing { + [self updateAppDefinedUserData]; + + if (self.requireUserName == BITFeedbackUserDataElementRequired && !self.userName) + return YES; + + if (self.requireUserEmail == BITFeedbackUserDataElementRequired && !self.userEmail) + return YES; + + return NO; +} + +- (BOOL)isManualUserDataAvailable { + [self updateAppDefinedUserData]; + + if ((self.requireUserName != BITFeedbackUserDataElementDontShow && self.userName) || + (self.requireUserEmail != BITFeedbackUserDataElementDontShow && self.userEmail)) + return YES; + + return NO; +} + + +#pragma mark - Networking + +- (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { + if (!jsonDictionary) { + // nil is used when the server returns 404, so we need to mark all existing threads as archives and delete the discussion token + + NSArray *messagesSendInProgress = [self messagesWithStatus:BITFeedbackMessageStatusSendInProgress]; + NSInteger pendingMessagesCount = [messagesSendInProgress count] + [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count]; + + [self markSendInProgressMessagesAsPending]; + + [self.feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger __unused messagesIdx, BOOL __unused *stop) { + if ([(BITFeedbackMessage *) objMessage status] != BITFeedbackMessageStatusSendPending) + [(BITFeedbackMessage *) objMessage setStatus:BITFeedbackMessageStatusArchived]; + }]; + + if ([self token]) { + self.token = nil; + } + + NSInteger pendingMessagesCountAfterProcessing = [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count]; + + [self saveMessages]; + + // check if this request was successful and we have more messages pending and continue if positive + if (pendingMessagesCount > pendingMessagesCountAfterProcessing && pendingMessagesCountAfterProcessing > 0) { + [self performSelector:@selector(submitPendingMessages) withObject:nil afterDelay:0.1]; + } + + return; + } + + NSDictionary *feedback = [jsonDictionary objectForKey:@"feedback"]; + NSString *token = [jsonDictionary objectForKey:@"token"]; + NSDictionary *feedbackObject = [jsonDictionary objectForKey:@"feedback"]; + if (feedback && token && feedbackObject) { + if ([self shouldForceNewThread]) { + self.token = nil; + } else { + // update the thread token, which is not available until the 1st message was successfully sent + self.token = token; + } + + self.lastCheck = [NSDate date]; + + // add all new messages + NSArray *feedMessages = [feedbackObject objectForKey:@"messages"]; + + // get the message that was currently sent if available + NSArray *messagesSendInProgress = [self messagesWithStatus:BITFeedbackMessageStatusSendInProgress]; + + NSInteger pendingMessagesCount = [messagesSendInProgress count] + [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count]; + + __block BOOL newMessage = NO; + NSMutableSet *returnedMessageIDs = [[NSMutableSet alloc] init]; + + [feedMessages enumerateObjectsUsingBlock:^(id objMessage, NSUInteger __unused messagesIdx, BOOL __unused *stop) { + if ([(NSDictionary *) objMessage objectForKey:@"id"]) { + NSNumber *messageID = [(NSDictionary *) objMessage objectForKey:@"id"]; + [returnedMessageIDs addObject:messageID]; + + BITFeedbackMessage *thisMessage = [self messageWithID:messageID]; + if (!thisMessage) { + // check if this is a message that was sent right now + __block BITFeedbackMessage *matchingSendInProgressOrInConflictMessage = nil; + + // TODO: match messages in state conflict + + [messagesSendInProgress enumerateObjectsUsingBlock:^(id objSendInProgressMessage, NSUInteger __unused messagesSendInProgressIdx, BOOL *stop2) { + if ([[(NSDictionary *) objMessage objectForKey:@"token"] isEqualToString:[(BITFeedbackMessage *) objSendInProgressMessage token]]) { + matchingSendInProgressOrInConflictMessage = objSendInProgressMessage; + *stop2 = YES; + } + }]; + + if (matchingSendInProgressOrInConflictMessage) { + matchingSendInProgressOrInConflictMessage.date = [self parseRFC3339Date:[(NSDictionary *) objMessage objectForKey:@"created_at"]]; + matchingSendInProgressOrInConflictMessage.identifier = messageID; + matchingSendInProgressOrInConflictMessage.status = BITFeedbackMessageStatusRead; + NSArray *feedbackAttachments = [(NSDictionary *) objMessage objectForKey:@"attachments"]; + if (matchingSendInProgressOrInConflictMessage.attachments.count == feedbackAttachments.count) { + int attachmentIndex = 0; + for (BITFeedbackMessageAttachment *attachment in matchingSendInProgressOrInConflictMessage.attachments) { + attachment.identifier = feedbackAttachments[attachmentIndex][@"id"]; + attachment.sourceURL = feedbackAttachments[attachmentIndex][@"url"]; + attachmentIndex++; + } + } + } else { + if ([(NSDictionary *) objMessage objectForKey:@"clean_text"] || [(NSDictionary *) objMessage objectForKey:@"text"] || [(NSDictionary *) objMessage objectForKey:@"attachments"]) { + BITFeedbackMessage *message = [[BITFeedbackMessage alloc] init]; + message.text = [(NSDictionary *) objMessage objectForKey:@"clean_text"] ?: [(NSDictionary *) objMessage objectForKey:@"text"] ?: @""; + message.name = [(NSDictionary *) objMessage objectForKey:@"name"] ?: @""; + message.email = [(NSDictionary *) objMessage objectForKey:@"email"] ?: @""; + + message.date = [self parseRFC3339Date:[(NSDictionary *) objMessage objectForKey:@"created_at"]] ?: [NSDate date]; + message.identifier = [(NSDictionary *) objMessage objectForKey:@"id"]; + message.status = BITFeedbackMessageStatusUnread; + + for (NSDictionary *attachmentData in objMessage[@"attachments"]) { + BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment new]; + newAttachment.originalFilename = attachmentData[@"file_name"]; + newAttachment.identifier = attachmentData[@"id"]; + newAttachment.sourceURL = attachmentData[@"url"]; + newAttachment.contentType = attachmentData[@"content_type"]; + [message addAttachmentsObject:newAttachment]; + } + + [self.feedbackList addObject:message]; + + newMessage = YES; + } + } + } else { + // we should never get any messages back that are already stored locally, + // since we add the last_message_id to the request + } + } + }]; + + [self markSendInProgressMessagesAsPending]; + + [self sortFeedbackList]; + [self updateLastMessageID]; + + // we got a new incoming message, trigger user notification system + if (newMessage) { + // check if the latest message is from the users own email address, then don't show an alert since they answered using their own email + BOOL latestMessageFromUser = NO; + + BITFeedbackMessage *latestMessage = [self lastMessageHavingID]; + if (self.userEmail && latestMessage.email && [self.userEmail compare:latestMessage.email] == NSOrderedSame) + latestMessageFromUser = YES; + id strongDelegate = self.delegate; + if (!latestMessageFromUser) { + if ([strongDelegate respondsToSelector:@selector(feedbackManagerDidReceiveNewFeedback:)]) { + [strongDelegate feedbackManagerDidReceiveNewFeedback:self]; + } + + if (self.showAlertOnIncomingMessages && !self.currentFeedbackListViewController && !self.currentFeedbackComposeViewController) { + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageTitle") + message:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageText") + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *cancelAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackIgnore") + style:UIAlertActionStyleCancel + handler:nil]; + UIAlertAction *showAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackShow") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused * __nonnull action) { + [self showFeedbackListView]; + }]; + [alertController addAction:cancelAction]; + [alertController addAction:showAction]; + + [self showAlertController:alertController]; + self.incomingMessagesAlertShowing = YES; + }); + } + } + } + + NSInteger pendingMessagesCountAfterProcessing = [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count]; + + // check if this request was successful and we have more messages pending and continue if positive + if (pendingMessagesCount > pendingMessagesCountAfterProcessing && pendingMessagesCountAfterProcessing > 0) { + [self performSelector:@selector(submitPendingMessages) withObject:nil afterDelay:0.1]; + } + + } else { + [self markSendInProgressMessagesAsPending]; + } + + [self saveMessages]; + + return; +} + + +- (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withMessage:(BITFeedbackMessage *)message completionHandler:(void (^)(NSError *error))completionHandler { + NSString *boundary = @"----FOO"; + + self.networkRequestInProgress = YES; + // inform the UI to update its data in case the list is already showing + [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingStarted object:nil]; + + NSString *tokenParameter = @""; + if ([self token]) { + tokenParameter = [NSString stringWithFormat:@"/%@", [self token]]; + } + NSMutableString *parameter = [NSMutableString stringWithFormat:@"api/2/apps/%@/feedback%@", [self encodedAppIdentifier], tokenParameter]; + + NSString *lastMessageID = @""; + if (!self.lastMessageID) { + [self updateLastMessageID]; + } + if (self.lastMessageID) { + lastMessageID = [NSString stringWithFormat:@"&last_message_id=%li", (long) [self.lastMessageID integerValue]]; + } + + [parameter appendFormat:@"?format=json&bundle_version=%@&sdk=%@&sdk_version=%@%@", + bit_URLEncodedString([[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]), + BITHOCKEY_NAME, + BITHOCKEY_VERSION, + lastMessageID + ]; + + // build request & send + NSString *url = [NSString stringWithFormat:@"%@%@", self.serverURL, parameter]; + BITHockeyLogDebug(@"INFO: sending api request to %@", url); + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:(NSURL *)[NSURL URLWithString:url] cachePolicy:1 timeoutInterval:10.0]; + [request setHTTPMethod:httpMethod]; + [request setValue:@"Hockey/iOS" forHTTPHeaderField:@"User-Agent"]; + [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"]; + + if (message) { + NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; + [request setValue:contentType forHTTPHeaderField:@"Content-type"]; + + NSMutableData *postBody = [NSMutableData data]; + + [postBody appendData:[BITHockeyAppClient dataWithPostValue:@"Apple" forKey:@"oem" boundary:boundary]]; + [postBody appendData:[BITHockeyAppClient dataWithPostValue:[[UIDevice currentDevice] systemVersion] forKey:@"os_version" boundary:boundary]]; + [postBody appendData:[BITHockeyAppClient dataWithPostValue:[self getDevicePlatform] forKey:@"model" boundary:boundary]]; + [postBody appendData:[BITHockeyAppClient dataWithPostValue:[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0] forKey:@"lang" boundary:boundary]]; + [postBody appendData:[BITHockeyAppClient dataWithPostValue:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] forKey:@"bundle_version" boundary:boundary]]; + [postBody appendData:[BITHockeyAppClient dataWithPostValue:[message text] forKey:@"text" boundary:boundary]]; + [postBody appendData:[BITHockeyAppClient dataWithPostValue:[message token] forKey:@"message_token" boundary:boundary]]; + + NSString *installString = bit_appAnonID(NO); + if (installString) { + [postBody appendData:[BITHockeyAppClient dataWithPostValue:installString forKey:@"install_string" boundary:boundary]]; + } + + if (self.userID) { + [postBody appendData:[BITHockeyAppClient dataWithPostValue:self.userID forKey:@"user_string" boundary:boundary]]; + } + if (self.userName) { + [postBody appendData:[BITHockeyAppClient dataWithPostValue:self.userName forKey:@"name" boundary:boundary]]; + } + if (self.userEmail) { + [postBody appendData:[BITHockeyAppClient dataWithPostValue:self.userEmail forKey:@"email" boundary:boundary]]; + } + + + NSInteger photoIndex = 0; + + for (BITFeedbackMessageAttachment *attachment in message.attachments) { + NSString *key = [NSString stringWithFormat:@"attachment%ld", (long) photoIndex]; + + NSString *filename = attachment.originalFilename; + + if (!filename) { + filename = [NSString stringWithFormat:@"Attachment %ld", (long) photoIndex]; + } + + [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.data forKey:key contentType:attachment.contentType boundary:boundary filename:filename]]; + + photoIndex++; + } + + [postBody appendData:(NSData *)[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + + + [request setHTTPBody:postBody]; + } + __weak typeof(self) weakSelf = self; + NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + __block NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; + + NSURLSessionDataTask *task = [session dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + typeof(self) strongSelf = weakSelf; + + [session finishTasksAndInvalidate]; + + [strongSelf handleFeedbackMessageResponse:response data:data error:error completion:completionHandler]; + }]; + [task resume]; + +} + +- (void)handleFeedbackMessageResponse:(NSURLResponse *)response data:(NSData *)responseData error:(NSError *)error completion:(void (^)(NSError *error))completionHandler { + self.networkRequestInProgress = NO; + + if (error) { + [self reportError:error]; + [self markSendInProgressMessagesAsPending]; + if (completionHandler) { + completionHandler(error); + } + } else { + NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode]; + if (statusCode == 404) { + // thread has been deleted, we archive it + [self updateMessageListFromResponse:nil]; + } else if (statusCode == 409) { + // we submitted a message that is already on the server, mark it as being in conflict and resolve it with another fetch + + if (!self.token) { + // set the token to the first message token, since this is identical + __block NSString *token = nil; + + [self.feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger __unused messagesIdx, BOOL *stop) { + if ([(BITFeedbackMessage *) objMessage status] == BITFeedbackMessageStatusSendInProgress) { + token = [(BITFeedbackMessage *) objMessage token]; + *stop = YES; + } + }]; + + if (token) { + self.token = token; + } + } + + [self markSendInProgressMessagesAsInConflict]; + [self saveMessages]; + [self performSelector:@selector(fetchMessageUpdates) withObject:nil afterDelay:0.2]; + } else if ([responseData length]) { + NSString *responseString = [[NSString alloc] initWithBytes:[responseData bytes] length:[responseData length] encoding:NSUTF8StringEncoding]; + BITHockeyLogDebug(@"INFO: Received API response: %@", responseString); + + if (responseString && [responseString dataUsingEncoding:NSUTF8StringEncoding]) { + NSError *localError = NULL; + + NSDictionary *feedDict = (NSDictionary *) [NSJSONSerialization JSONObjectWithData:(NSData *)[responseString dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&localError]; + + // server returned empty response? + if (localError) { + [self reportError:localError]; + } else if (![feedDict count]) { + [self reportError:[NSError errorWithDomain:kBITFeedbackErrorDomain + code:BITFeedbackAPIServerReturnedEmptyResponse + userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Server returned empty response.", NSLocalizedDescriptionKey, nil]]]; + } else { + BITHockeyLogDebug(@"INFO: Received API response: %@", responseString); + NSString *status = [feedDict objectForKey:@"status"]; + if ([status compare:@"success"] != NSOrderedSame) { + [self reportError:[NSError errorWithDomain:kBITFeedbackErrorDomain + code:BITFeedbackAPIServerReturnedInvalidStatus + userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Server returned invalid status.", NSLocalizedDescriptionKey, nil]]]; + } else { + [self updateMessageListFromResponse:feedDict]; + } + } + } + } + + [self markSendInProgressMessagesAsPending]; + if (completionHandler) { + completionHandler(error); + } + } +} + +- (void)fetchMessageUpdates { + if ([self.feedbackList count] == 0 && !self.token) { + // inform the UI to update its data in case the list is already showing + [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingFinished object:nil]; + + return; + } + + [self sendNetworkRequestWithHTTPMethod:@"GET" + withMessage:nil + completionHandler:^(NSError __unused *error) { + // inform the UI to update its data in case the list is already showing + [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingFinished object:nil]; + }]; +} + +- (void)submitPendingMessages { + if (self.networkRequestInProgress) { + [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(submitPendingMessages) object:nil]; + [self performSelector:@selector(submitPendingMessages) withObject:nil afterDelay:2.0]; + return; + } + + // app defined user data may have changed, update it + [self updateAppDefinedUserData]; + [self saveMessages]; + + NSArray *pendingMessages = [self messagesWithStatus:BITFeedbackMessageStatusSendPending]; + + if ([pendingMessages count] > 0) { + // we send one message at a time + BITFeedbackMessage *messageToSend = pendingMessages[0]; + + [messageToSend setStatus:BITFeedbackMessageStatusSendInProgress]; + if (self.userID) + [messageToSend setUserID:self.userID]; + if (self.userName) + [messageToSend setName:self.userName]; + if (self.userEmail) + [messageToSend setEmail:self.userEmail]; + + NSString *httpMethod = @"POST"; + if ([self token]) { + httpMethod = @"PUT"; + } + + [self sendNetworkRequestWithHTTPMethod:httpMethod + withMessage:messageToSend + completionHandler:^(NSError *error) { + if (error) { + [self markSendInProgressMessagesAsPending]; + [self saveMessages]; + } + + // inform the UI to update its data in case the list is already showing + [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingFinished object:nil]; + }]; + } +} + +- (void)submitMessageWithText:(NSString *)text andAttachments:(NSArray *)attachments { + BITFeedbackMessage *message = [[BITFeedbackMessage alloc] init]; + message.text = text; + [message setStatus:BITFeedbackMessageStatusSendPending]; + [message setToken:[self uuidAsLowerCaseAndShortened]]; + [message setAttachments:attachments]; + [message setUserMessage:YES]; + + [self.feedbackList addObject:message]; + + [self submitPendingMessages]; +} + +#pragma mark - Observation Handling + +- (void)setFeedbackObservationMode:(BITFeedbackObservationMode)feedbackObservationMode { + //Ignore if feedback manager is disabled + if ([self isFeedbackManagerDisabled]) return; + + if (feedbackObservationMode != _feedbackObservationMode) { + _feedbackObservationMode = feedbackObservationMode; + + // Reset the other observation modes. + if (feedbackObservationMode == BITFeedbackObservationNone) { + if (self.observationModeOnScreenshotEnabled) { + [self setObservationModeOnScreenshotEnabled:NO]; + } + if (self.observationModeThreeFingerTapEnabled) { + [self setObservationModeThreeFingerTapEnabled:NO]; + } + BITHockeyLogVerbose(@"Set feedbackObservationMode to BITFeedbackObservationNone"); + } + + if (feedbackObservationMode == BITFeedbackObservationModeOnScreenshot) { + [self setObservationModeOnScreenshotEnabled:YES]; + if (self.observationModeThreeFingerTapEnabled) { + [self setObservationModeThreeFingerTapEnabled:NO]; + } + } + + if (feedbackObservationMode == BITFeedbackObservationModeThreeFingerTap) { + [self setObservationModeThreeFingerTapEnabled:YES]; + if (self.observationModeOnScreenshotEnabled) { + [self setObservationModeOnScreenshotEnabled:NO]; + } + } + + if (feedbackObservationMode == BITFeedbackObservationModeAll) { + [self setObservationModeOnScreenshotEnabled:YES]; + [self setObservationModeThreeFingerTapEnabled:YES]; + } + } +} + +- (void)setObservationModeOnScreenshotEnabled:(BOOL)observationModeOnScreenshotEnabled { + // Enable/disable screenshot notification + if (observationModeOnScreenshotEnabled) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + BITHockeyLogVerbose(@"Added observer for UIApplocationUserDidTakeScreenshotNotification."); + } else { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + BITHockeyLogVerbose(@"Removed observer for UIApplocationUserDidTakeScreenshotNotification."); + } + + _observationModeOnScreenshotEnabled = observationModeOnScreenshotEnabled; + + BITHockeyLogVerbose(@"Enabled BITFeedbackObservationModeOnScreenshot."); +} + +- (void)setObservationModeThreeFingerTapEnabled:(BOOL)observationModeThreeFingerTapEnabled { + _observationModeThreeFingerTapEnabled = observationModeThreeFingerTapEnabled; + + if (observationModeThreeFingerTapEnabled) { + if (!self.tapRecognizer) { + self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(screenshotTripleTap:)]; + self.tapRecognizer.numberOfTouchesRequired = 3; + self.tapRecognizer.delegate = self; + + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.tapRecognizer) { + [[UIApplication sharedApplication].keyWindow addGestureRecognizer:self.tapRecognizer]; + } + }); + } + + BITHockeyLogVerbose(@"Enabled BITFeedbackObservationModeThreeFingerTap."); + } else { + [[[UIApplication sharedApplication] keyWindow] removeGestureRecognizer:self.tapRecognizer]; + self.tapRecognizer = nil; + BITHockeyLogVerbose(@"Disabled BITFeedbackObservationModeThreeFingerTap."); + } +} + +- (void)screenshotNotificationReceived:(NSNotification *) __unused notification { + // Don't do anything if FeedbackManager was disabled. + if ([self isFeedbackManagerDisabled]) return; + + double amountOfSeconds = 1.5; + dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (amountOfSeconds * NSEC_PER_SEC)); + + dispatch_after(delayTime, dispatch_get_main_queue(), ^{ + [self extractLastPictureFromLibraryAndLaunchFeedback]; + }); +} + +- (void)extractLastPictureFromLibraryAndLaunchFeedback { + [self requestLatestImageWithCompletionHandler:^(UIImage *latestImage) { + [self showFeedbackComposeViewWithPreparedItems:@[latestImage]]; + }]; +} + +- (void)requestLatestImageWithCompletionHandler:(BITLatestImageFetchCompletionBlock)completionHandler { + if (!completionHandler) {return;} + + // Safeguard in case the dev hasn't set the NSPhotoLibraryUsageDescription in their Info.plist + if (![self isiOS10PhotoPolicySet]) {return;} + + [self fetchLatestImageUsingPhotoLibraryWithCompletionHandler:completionHandler]; +} + +- (void)fetchLatestImageUsingPhotoLibraryWithCompletionHandler:(BITLatestImageFetchCompletionBlock)completionHandler { + // Safeguard in case the dev hasn't set the NSPhotoLibraryUsageDescription in their Info.plist + if (![self isiOS10PhotoPolicySet]) {return;} + + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { + switch (status) { + case PHAuthorizationStatusDenied: + case PHAuthorizationStatusRestricted: + BITHockeyLogDebug(@"INFO: The latest image could not be fetched, no permissions."); + break; + + case PHAuthorizationStatusAuthorized: + [self loadLatestImageAssetWithCompletionHandler:completionHandler]; + break; + case PHAuthorizationStatusNotDetermined: + BITHockeyLogDebug(@"INFO: The Photo Library authorization status is undetermined. This should not happen."); + break; + } + }]; +} + +- (void)loadLatestImageAssetWithCompletionHandler:(BITLatestImageFetchCompletionBlock)completionHandler { + + // Safeguard in case the dev hasn't set the NSPhotoLibraryUsageDescription in their Info.plist + if (![self isiOS10PhotoPolicySet]) {return;} + + PHImageManager *imageManager = PHImageManager.defaultManager; + + PHFetchOptions *fetchOptions = [PHFetchOptions new]; + fetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]]; + + PHFetchResult *fetchResult = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:fetchOptions]; + + if (fetchResult.count > 0) { + PHAsset *latestImageAsset = (PHAsset *) fetchResult.lastObject; + if (latestImageAsset) { + PHImageRequestOptions *options = [PHImageRequestOptions new]; + options.version = PHImageRequestOptionsVersionOriginal; + options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + options.resizeMode = PHImageRequestOptionsResizeModeNone; + + [imageManager requestImageDataForAsset:latestImageAsset options:options resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable __unused dataUTI, UIImageOrientation __unused orientation, NSDictionary *_Nullable __unused info) { + if (imageData) { + completionHandler((UIImage *)[UIImage imageWithData:(NSData *)imageData]); + } else { + BITHockeyLogDebug(@"INFO: The latest image could not be fetched, requested image data was empty."); + } + }]; + } + } else { + BITHockeyLogDebug(@"INFO: The latest image could not be fetched, the fetch result was empty."); + } +} + +- (void)screenshotTripleTap:(UITapGestureRecognizer *)tapRecognizer { + // Don't do anything if FeedbackManager was disabled. + if ([self isFeedbackManagerDisabled]) return; + + if (tapRecognizer.state == UIGestureRecognizerStateRecognized) { + [self showFeedbackComposeViewWithGeneratedScreenshot]; + } +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *) __unused gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *) __unused otherGestureRecognizer { + return YES; +} + +@end + + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackManagerDelegate.h b/submodules/HockeySDK-iOS/Classes/BITFeedbackManagerDelegate.h new file mode 100644 index 0000000000..9afb889462 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackManagerDelegate.h @@ -0,0 +1,97 @@ +/* + * Authors: Stephan Diederich, Benjamin Scholtysik + * + * Copyright (c) 2013-2016 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 +#import "BITFeedbackComposeViewControllerDelegate.h" + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +@class BITFeedbackManager; + +/** + * Delegate protocol which is notified about changes in the feedbackManager + * @TODO + * * move shouldShowUpdateAlert from feedbackManager here + */ +@protocol BITFeedbackManagerDelegate + +@optional + +/** + * Can be implemented to control wether the feedback manager should automatically + * fetch for new messages on app startup or when becoming active. + * + * By default the SDK fetches on app startup or when the app is becoming active again + * if there are already messages existing or pending on the device. + * + * You could disable it e.g. depending on available mobile network/WLAN connection + * or let it fetch less frequently. + * + * @param feedbackManager The feedbackManager which did detect the new messages + */ +- (BOOL)allowAutomaticFetchingForNewFeedbackForManager:(BITFeedbackManager *)feedbackManager; + + +/** + * can be implemented to know when new feedback from the server arrived + * + * @param feedbackManager The feedbackManager which did detect the new messages + */ +- (void)feedbackManagerDidReceiveNewFeedback:(BITFeedbackManager *)feedbackManager; + + +/** + * This optional method can be implemented to provide items to prefill + * the FeedbackComposeMessage user interface with the given items. + * + * If the user sends the feedback message, these items will be attached to that message. + * + * All NSString-Content in the array will be concatenated and result in the message, + * while all UIImage and NSData-instances will be turned into attachments. + * + * @param feedbackManager The BITFeedbackManager instance that will handle sending the feedback. + * + * @return An array containing the items to be attached to the feedback message + * @see `[BITFeedbackComposeViewController prepareWithItems:] + */ +- (nullable NSArray *)preparedItemsForFeedbackManager:(BITFeedbackManager *)feedbackManager; + +/** + * Indicates if a new thread should be created for each new feedback message + * + * Setting it to `YES` will force a new thread whenever a new message is sent as + * opposed to the default resume thread behaviour. + * + * @return A BOOL indicating if each feedback message should be sent as a new thread. + */ +- (BOOL)forceNewFeedbackThreadForFeedbackManager:(BITFeedbackManager *)feedbackManager; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackManagerPrivate.h b/submodules/HockeySDK-iOS/Classes/BITFeedbackManagerPrivate.h new file mode 100644 index 0000000000..ab4bbe1935 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackManagerPrivate.h @@ -0,0 +1,133 @@ +/* + * Author: Andreas Linde + * Kent Sutherland + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde & Kent Sutherland. + * 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. + */ + +#if HOCKEYSDK_FEATURE_FEEDBACK + +extern NSString *const kBITFeedbackUpdateAttachmentThumbnail; + +#import "BITFeedbackMessage.h" + +@class UITapGestureRecognizer; + +@interface BITFeedbackManager () { +} + + +///----------------------------------------------------------------------------- +/// @name Delegate +///----------------------------------------------------------------------------- + +/** + Sets the `BITFeedbackManagerDelegate` delegate. + + Can be set to be notified when new feedback is received from the server. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see `[BITHockeyManager setDelegate:]` + */ +@property (nonatomic, weak) id delegate; + + +@property (nonatomic, strong) NSMutableArray *feedbackList; +@property (nonatomic, copy) NSString *token; + + +// used by BITHockeyManager if disable status is changed +@property (nonatomic, getter = isFeedbackManagerDisabled) BOOL disableFeedbackManager; +// TapRecognizer used in case feedback observation mode is BITFeedbackObservationModeThreeFingerTap is set. +@property(nonatomic, strong) UITapGestureRecognizer *tapRecognizer; +@property(nonatomic) BOOL observationModeOnScreenshotEnabled; +@property(nonatomic) BOOL observationModeThreeFingerTapEnabled; + + +@property (nonatomic, strong) BITFeedbackListViewController *currentFeedbackListViewController; +@property (nonatomic, strong) BITFeedbackComposeViewController *currentFeedbackComposeViewController; +@property (nonatomic) BOOL didAskUserData; + +@property (nonatomic, strong) NSDate *lastCheck; + +@property (nonatomic, strong) NSNumber *lastMessageID; + +@property (nonatomic, copy) NSString *userID; +@property (nonatomic, copy) NSString *userName; +@property (nonatomic, copy) NSString *userEmail; + + + +// Fetch user meta data +- (BOOL)updateUserIDUsingKeychainAndDelegate; +- (BOOL)updateUserNameUsingKeychainAndDelegate; +- (BOOL)updateUserEmailUsingKeychainAndDelegate; + +// check if the user wants to influence when fetching of new messages may be done +- (BOOL)allowFetchingNewMessages; + +// load new messages from the server +- (void)updateMessagesList; + +// load new messages from the server if the last request is too long ago +- (void)updateMessagesListIfRequired; + +- (NSUInteger)numberOfMessages; +- (BITFeedbackMessage *)messageAtIndex:(NSUInteger)index; + +- (void)submitMessageWithText:(NSString *)text andAttachments:(NSArray *)photos; +- (void)submitPendingMessages; + +// Returns YES if manual user data can be entered, required or optional +- (BOOL)askManualUserDataAvailable; + +// Returns YES if required user data is missing? +- (BOOL)requireManualUserDataMissing; + +// Returns YES if user data is available and can be edited +- (BOOL)isManualUserDataAvailable; + +// used in the user data screen +- (void)updateDidAskUserData; + + +- (BITFeedbackMessage *)messageWithID:(NSNumber *)messageID; + +- (NSArray *)messagesWithStatus:(BITFeedbackMessageStatus)status; + +- (void)saveMessages; + +- (void)fetchMessageUpdates; +- (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary; + +- (BOOL)deleteMessageAtIndex:(NSUInteger)index; +- (void)deleteAllMessages; + +@end + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackMessage.h b/submodules/HockeySDK-iOS/Classes/BITFeedbackMessage.h new file mode 100644 index 0000000000..c33caa983d --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackMessage.h @@ -0,0 +1,102 @@ +/* + * 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 + +@class BITFeedbackMessageAttachment; + +/** + * Status for each feedback message + */ +typedef NS_ENUM(NSInteger, BITFeedbackMessageStatus) { + /** + * default and new messages from SDK per default + */ + BITFeedbackMessageStatusSendPending = 0, + /** + * message is in conflict, happens if the message is already stored on the server and tried sending it again + */ + BITFeedbackMessageStatusInConflict = 1, + /** + * sending of message is in progress + */ + BITFeedbackMessageStatusSendInProgress = 2, + /** + * new messages from server + */ + BITFeedbackMessageStatusUnread = 3, + /** + * messages from server once read and new local messages once successful send from SDK + */ + BITFeedbackMessageStatusRead = 4, + /** + * message is archived, happens if the thread is deleted from the server + */ + BITFeedbackMessageStatusArchived = 5 +}; + +/** + * An individual feedback message + */ +@interface BITFeedbackMessage : NSObject { +} + +@property (nonatomic, copy) NSString *text; +@property (nonatomic, copy) NSString *userID; +@property (nonatomic, copy) NSString *name; +@property (nonatomic, copy) NSString *email; +@property (nonatomic, copy) NSDate *date; +@property (nonatomic, copy) NSNumber *identifier; +@property (nonatomic, copy) NSString *token; +@property (nonatomic, strong) NSArray *attachments; +@property (nonatomic) BITFeedbackMessageStatus status; +@property (nonatomic) BOOL userMessage; + +/** + Delete local cached attachment data + + @warning This method must be called before a feedback message is deleted. + */ +- (void)deleteContents; + +/** + Add an attachment to a message + + @param object BITFeedbackMessageAttachment instance representing the attachment that should be added + */ +- (void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object; + +/** + Return the attachments that can be viewed + + @return NSArray containing the attachment objects that can be previewed + */ +- (NSArray *)previewableAttachments; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackMessage.m b/submodules/HockeySDK-iOS/Classes/BITFeedbackMessage.m new file mode 100644 index 0000000000..e0c05c4055 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackMessage.m @@ -0,0 +1,120 @@ +/* + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "BITFeedbackMessage.h" +#import "BITFeedbackMessageAttachment.h" + +@implementation BITFeedbackMessage + + +#pragma mark - NSObject + +- (instancetype) init { + if ((self = [super init])) { + _text = nil; + _userID = nil; + _name = nil; + _email = nil; + _date = [[NSDate alloc] init]; + _token = nil; + _attachments = nil; + _identifier = [[NSNumber alloc] initWithInteger:0]; + _status = BITFeedbackMessageStatusSendPending; + _userMessage = NO; + } + return self; +} + + +#pragma mark - NSCoder + +- (void)encodeWithCoder:(NSCoder *)encoder { + [encoder encodeObject:self.text forKey:@"text"]; + [encoder encodeObject:self.userID forKey:@"userID"]; + [encoder encodeObject:self.name forKey:@"name"]; + [encoder encodeObject:self.email forKey:@"email"]; + [encoder encodeObject:self.date forKey:@"date"]; + [encoder encodeObject:self.identifier forKey:@"id"]; + [encoder encodeObject:self.attachments forKey:@"attachments"]; + [encoder encodeInteger:self.status forKey:@"status"]; + [encoder encodeBool:self.userMessage forKey:@"userMessage"]; + [encoder encodeObject:self.token forKey:@"token"]; +} + +- (instancetype)initWithCoder:(NSCoder *)decoder { + if ((self = [self init])) { + self.text = [decoder decodeObjectForKey:@"text"]; + self.userID = [decoder decodeObjectForKey:@"userID"]; + self.name = [decoder decodeObjectForKey:@"name"]; + self.email = [decoder decodeObjectForKey:@"email"]; + self.date = [decoder decodeObjectForKey:@"date"]; + self.identifier = [decoder decodeObjectForKey:@"id"]; + self.attachments = [decoder decodeObjectForKey:@"attachments"]; + self.status = (BITFeedbackMessageStatus)[decoder decodeIntegerForKey:@"status"]; + self.userMessage = [decoder decodeBoolForKey:@"userMessage"]; + self.token = [decoder decodeObjectForKey:@"token"]; + } + return self; +} + +#pragma mark - Deletion + +- (void)deleteContents { + for (BITFeedbackMessageAttachment *attachment in self.attachments){ + [attachment deleteContents]; + } +} + +- (NSArray *)previewableAttachments { + NSMutableArray *returnArray = [NSMutableArray new]; + + for (BITFeedbackMessageAttachment *attachment in self.attachments){ + if ([QLPreviewController canPreviewItem:attachment ]){ + [returnArray addObject:attachment]; + } + } + + return returnArray; +} + +- (void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object{ + if (!self.attachments){ + self.attachments = [NSArray array]; + } + self.attachments = [self.attachments arrayByAddingObject:object]; +} + + +@end + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackMessageAttachment.h b/submodules/HockeySDK-iOS/Classes/BITFeedbackMessageAttachment.h new file mode 100644 index 0000000000..bdfaec0085 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackMessageAttachment.h @@ -0,0 +1,68 @@ +/* + * Author: Moritz Haarmann + * + * 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 +#import +#import + +/** + * An individual feedback message attachment + */ +@interface BITFeedbackMessageAttachment : NSObject + +@property (nonatomic, copy) NSNumber *identifier; +@property (nonatomic, copy) NSString *originalFilename; +@property (nonatomic, copy) NSString *contentType; +@property (nonatomic, copy) NSString *sourceURL; +@property (nonatomic) BOOL isLoading; +@property (nonatomic, readonly) NSData *data; + + +@property (nonatomic, readonly) UIImage *imageRepresentation; + + ++ (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType:(NSString *)contentType; + +- (UIImage *)thumbnailWithSize:(CGSize)size; + +- (void)replaceData:(NSData *)data; + +- (void)deleteContents; + +- (BOOL)needsLoadingFromURL; + +- (BOOL)isImage; + +- (NSURL *)localURL; + +/** + Used to determine whether QuickLook can preview this file or not. If not, we don't download it. + */ +- (NSString*)possibleFilename; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackMessageAttachment.m b/submodules/HockeySDK-iOS/Classes/BITFeedbackMessageAttachment.m new file mode 100644 index 0000000000..1ea27ed79d --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackMessageAttachment.m @@ -0,0 +1,269 @@ +/* + * Author: Moritz Haarmann + * + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "BITFeedbackMessageAttachment.h" +#import "BITHockeyHelper.h" +#import "HockeySDKPrivate.h" +#import + +#define kCacheFolderName @"attachments" + +@interface BITFeedbackMessageAttachment() + +@property (nonatomic, strong) NSMutableDictionary *thumbnailRepresentations; +@property (nonatomic, strong) NSData *internalData; +@property (nonatomic, copy) NSString *filename; +@property (nonatomic, copy) NSString *tempFilename; +@property (nonatomic, copy) NSString *cachePath; +@property (nonatomic, strong) NSFileManager *fm; + +@end + +@implementation BITFeedbackMessageAttachment + ++ (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType:(NSString *)contentType { + + static NSDateFormatter *formatter; + + if(!formatter) { + formatter = [NSDateFormatter new]; + formatter.dateStyle = NSDateFormatterShortStyle; + formatter.timeStyle = NSDateFormatterShortStyle; + } + + BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment new]; + newAttachment.contentType = contentType; + newAttachment.data = data; + newAttachment.originalFilename = [NSString stringWithFormat:@"Attachment: %@", [formatter stringFromDate:[NSDate date]]]; + + return newAttachment; +} + +- (instancetype)init { + if ((self = [super init])) { + _isLoading = NO; + _thumbnailRepresentations = [NSMutableDictionary new]; + + _fm = [[NSFileManager alloc] init]; + _cachePath = [bit_settingsDir() stringByAppendingPathComponent:kCacheFolderName]; + + BOOL isDirectory; + + if (![_fm fileExistsAtPath:_cachePath isDirectory:&isDirectory]){ + [_fm createDirectoryAtPath:_cachePath withIntermediateDirectories:YES attributes:nil error:nil]; + } + + } + return self; +} + +- (void)setData:(NSData *)data { + self.internalData = data; + self.filename = [self possibleFilename]; + [self.internalData writeToFile:self.filename atomically:YES]; +} + +- (NSData *)data { + if (!self.internalData && self.filename) { + self.internalData = [NSData dataWithContentsOfFile:self.filename]; + } + + if (self.internalData) { + return self.internalData; + } + + return [NSData data]; +} + +- (void)replaceData:(NSData *)data { + self.data = data; + self.thumbnailRepresentations = [NSMutableDictionary new]; +} + +- (BOOL)needsLoadingFromURL { + return (self.sourceURL && ![self.fm fileExistsAtPath:(NSString *)[self.localURL path]]); +} + +- (BOOL)isImage { + return ([self.contentType rangeOfString:@"image"].location != NSNotFound); +} + +- (NSURL *)localURL { + if (self.filename && [self.fm fileExistsAtPath:self.filename]) { + return [NSURL fileURLWithPath:self.filename]; + } + + return [NSURL URLWithString:@""]; +} + + +#pragma mark NSCoding + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.contentType forKey:@"contentType"]; + [aCoder encodeObject:self.filename forKey:@"filename"]; + [aCoder encodeObject:self.originalFilename forKey:@"originalFilename"]; + [aCoder encodeObject:self.sourceURL forKey:@"url"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if ((self = [self init])) { + self.contentType = [aDecoder decodeObjectForKey:@"contentType"]; + self.filename = [aDecoder decodeObjectForKey:@"filename"]; + self.thumbnailRepresentations = [NSMutableDictionary new]; + self.originalFilename = [aDecoder decodeObjectForKey:@"originalFilename"]; + self.sourceURL = [aDecoder decodeObjectForKey:@"url"]; + } + + return self; +} + + +#pragma mark - Thumbnails / Image Representation + +- (UIImage *)imageRepresentation { + if ([self.contentType rangeOfString:@"image"].location != NSNotFound && self.filename ) { + return [UIImage imageWithData:self.data]; + } else { + // Create a Icon .. + UIDocumentInteractionController* docController = [[UIDocumentInteractionController alloc] init]; + docController.name = self.originalFilename; + NSArray* icons = docController.icons; + if (icons.count){ + return icons[0]; + } else { + return nil; + } + } +} + +- (UIImage *)thumbnailWithSize:(CGSize)size { + id cacheKey = [NSValue valueWithCGSize:size]; + + if (!self.thumbnailRepresentations[cacheKey]) { + UIImage *image = self.imageRepresentation; + // consider the scale. + if (!image) { + return nil; + } + + CGFloat scale = [UIScreen mainScreen].scale; + + if (scale != image.scale) { + + CGSize scaledSize = CGSizeApplyAffineTransform(size, CGAffineTransformMakeScale(scale, scale)); + UIImage *thumbnail = bit_imageToFitSize(image, scaledSize, YES) ; + + UIImage *scaledThumbnail = [UIImage imageWithCGImage:(CGImageRef)thumbnail.CGImage scale:scale orientation:thumbnail.imageOrientation]; + if (thumbnail) { + [self.thumbnailRepresentations setObject:scaledThumbnail forKey:cacheKey]; + } + + } else { + UIImage *thumbnail = bit_imageToFitSize(image, size, YES) ; + + [self.thumbnailRepresentations setObject:thumbnail forKey:cacheKey]; + + } + + } + + return self.thumbnailRepresentations[cacheKey]; +} + + +#pragma mark - Persistence Helpers + +- (void)setFilename:(NSString *)filename { + if (filename) { + filename = [self.cachePath stringByAppendingPathComponent:[filename lastPathComponent]]; + } + _filename = filename; +} + +- (NSString *)possibleFilename { + if (self.tempFilename) { + return self.tempFilename; + } + + NSString *uniqueString = bit_UUID(); + self.tempFilename = [self.cachePath stringByAppendingPathComponent:uniqueString]; + + // File extension that suits the Content type. + + CFStringRef mimeType = (__bridge CFStringRef)self.contentType; + if (mimeType) { + CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL); + CFStringRef extension = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension); + if (extension) { + self.tempFilename = [self.tempFilename stringByAppendingPathExtension:(__bridge NSString *)(extension)]; + CFRelease(extension); + } + if (uti) { + CFRelease(uti); + } + } + + return self.tempFilename; +} + +- (void)deleteContents { + if (self.filename) { + [self.fm removeItemAtPath:self.filename error:nil]; + self.filename = nil; + } +} + + +#pragma mark - QLPreviewItem + +- (NSString *)previewItemTitle { + return self.originalFilename; +} + +- (NSURL *)previewItemURL { + if (self.localURL){ + return self.localURL; + } else if (self.sourceURL) { + NSString *filename = self.possibleFilename; + if (filename) { + return [NSURL fileURLWithPath:filename]; + } + } + + return [NSURL URLWithString:(NSString *)[[NSBundle mainBundle] pathForResource:@"FeedbackPlaceholder" ofType:@"png"]]; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackUserDataViewController.h b/submodules/HockeySDK-iOS/Classes/BITFeedbackUserDataViewController.h new file mode 100644 index 0000000000..84128d086d --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackUserDataViewController.h @@ -0,0 +1,51 @@ +/* + * 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 + +@protocol BITFeedbackUserDataDelegate; + +@interface BITFeedbackUserDataViewController : UITableViewController + +@property (nonatomic, weak) id delegate; + +@end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +@protocol BITFeedbackUserDataDelegate + +@required + +// cancel action is invoked +- (void)userDataUpdateCancelled; + +// save action is invoked and all required data available +- (void)userDataUpdateFinished; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITFeedbackUserDataViewController.m b/submodules/HockeySDK-iOS/Classes/BITFeedbackUserDataViewController.m new file mode 100644 index 0000000000..4b49a7e1ee --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITFeedbackUserDataViewController.m @@ -0,0 +1,266 @@ +/* + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "HockeySDKPrivate.h" +#import "BITHockeyHelper.h" + +#import "BITFeedbackUserDataViewController.h" +#import "BITFeedbackManagerPrivate.h" + +@interface BITFeedbackUserDataViewController () { +} + +@property (nonatomic, weak) BITFeedbackManager *manager; + +@property (nonatomic, copy) NSString *name; +@property (nonatomic, copy) NSString *email; +@end + + +@implementation BITFeedbackUserDataViewController + + +- (instancetype)initWithStyle:(UITableViewStyle)style { + self = [super initWithStyle:style]; + if (self) { + _delegate = nil; + + _manager = [BITHockeyManager sharedHockeyManager].feedbackManager; + _name = @""; + _email = @""; + } + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.title = BITHockeyLocalizedString(@"HockeyFeedbackUserDataTitle"); + + [self.tableView setScrollEnabled:NO]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + // Do any additional setup after loading the view. + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel + target:self + action:@selector(dismissAction:)]; + + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave + target:self + action:@selector(saveAction:)]; + + BITFeedbackManager *strongManager = self.manager; + if ([strongManager userName]) + self.name = [strongManager userName]; + + if ([strongManager userEmail]) + self.email = [strongManager userEmail]; + + [strongManager updateDidAskUserData]; + + self.navigationItem.rightBarButtonItem.enabled = [self allRequiredFieldsEntered]; +} + +#pragma mark - UIViewController Rotation + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations{ + return UIInterfaceOrientationMaskAll; +} + +#pragma mark - Private methods +- (BOOL)allRequiredFieldsEntered { + BITFeedbackManager *strongManager = self.manager; + if ([strongManager requireUserName] == BITFeedbackUserDataElementRequired && [self.name length] == 0) + return NO; + + if ([strongManager requireUserEmail] == BITFeedbackUserDataElementRequired && [self.email length] == 0) + return NO; + + if ([self.email length] > 0 && !bit_validateEmail(self.email)) + return NO; + + return YES; +} + +- (void)userNameEntered:(id)sender { + self.name = [(UITextField *)sender text]; + + self.navigationItem.rightBarButtonItem.enabled = [self allRequiredFieldsEntered]; +} + +- (void)userEmailEntered:(id)sender { + self.email = [(UITextField *)sender text]; + + self.navigationItem.rightBarButtonItem.enabled = [self allRequiredFieldsEntered]; +} + +- (void)dismissAction:(id) __unused sender { + [self.delegate userDataUpdateCancelled]; +} + +- (void)saveAction:(id) __unused sender { + BITFeedbackManager *strongManager = self.manager; + if ([strongManager requireUserName]) { + [strongManager setUserName:self.name]; + } + + if ([strongManager requireUserEmail]) { + [strongManager setUserEmail:self.email]; + } + + [self.delegate userDataUpdateFinished]; +} + +#pragma mark - Table view data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView *) __unused tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *) __unused tableView numberOfRowsInSection:(NSInteger) __unused section { + NSInteger rows = 0; + BITFeedbackManager *strongManager = self.manager; + if ([strongManager requireUserName] != BITFeedbackUserDataElementDontShow) + rows ++; + + if ([strongManager requireUserEmail] != BITFeedbackUserDataElementDontShow) + rows ++; + + return rows; +} + +- (NSString *)tableView:(UITableView *) __unused tableView titleForFooterInSection:(NSInteger)section { + if (section == 0) { + return BITHockeyLocalizedString(@"HockeyFeedbackUserDataDescription"); + } + + return nil; +} + +- (UITableViewCell *)tableView:(UITableView *) __unused tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *CellIdentifier = @"InputCell"; + BITFeedbackManager *strongManager = self.manager; + UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier]; + if (cell == nil) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; + + cell.accessoryType = UITableViewCellAccessoryNone; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + cell.backgroundColor = [UIColor whiteColor]; + + UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(110, 11, self.view.frame.size.width - 110 - 35, 24)]; + if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) { + textField.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin; + } + textField.adjustsFontSizeToFitWidth = YES; + textField.textColor = [UIColor blackColor]; + textField.backgroundColor = [UIColor lightGrayColor]; + + if ([indexPath row] == 0 && [strongManager requireUserName] != BITFeedbackUserDataElementDontShow) { + textField.placeholder = BITHockeyLocalizedString(@"HockeyFeedbackUserDataNamePlaceHolder"); + textField.text = self.name; + if (strongManager.requireUserName == BITFeedbackUserDataElementRequired) { + textField.accessibilityHint = BITHockeyLocalizedString(@"HockeyAccessibilityHintRequired"); + } + + textField.keyboardType = UIKeyboardTypeDefault; + if ([strongManager requireUserEmail]) + textField.returnKeyType = UIReturnKeyNext; + else + textField.returnKeyType = UIReturnKeyDone; + textField.autocapitalizationType = UITextAutocapitalizationTypeWords; + [textField addTarget:self action:@selector(userNameEntered:) forControlEvents:UIControlEventEditingChanged]; + [textField becomeFirstResponder]; + } else { + textField.placeholder = BITHockeyLocalizedString(@"HockeyFeedbackUserDataEmailPlaceholder"); + textField.text = self.email; + if (strongManager.requireUserEmail == BITFeedbackUserDataElementRequired) { + textField.accessibilityHint = BITHockeyLocalizedString(@"HockeyAccessibilityHintRequired"); + } + + textField.keyboardType = UIKeyboardTypeEmailAddress; + textField.returnKeyType = UIReturnKeyDone; + textField.autocapitalizationType = UITextAutocapitalizationTypeNone; + [textField addTarget:self action:@selector(userEmailEntered:) forControlEvents:UIControlEventEditingChanged]; + if (![strongManager requireUserName]) + [textField becomeFirstResponder]; + } + + textField.backgroundColor = [UIColor whiteColor]; + textField.autocorrectionType = UITextAutocorrectionTypeNo; + textField.textAlignment = NSTextAlignmentLeft; + textField.delegate = self; + textField.tag = indexPath.row; + + textField.clearButtonMode = UITextFieldViewModeWhileEditing; + [textField setEnabled: YES]; + + [cell addSubview:textField]; + } + + if ([indexPath row] == 0 && [strongManager requireUserName] != BITFeedbackUserDataElementDontShow) { + cell.textLabel.text = BITHockeyLocalizedString(@"HockeyFeedbackUserDataName"); + } else { + cell.textLabel.text = BITHockeyLocalizedString(@"HockeyFeedbackUserDataEmail"); + } + + return cell; +} + + +#pragma mark - UITextFieldDelegate + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + NSInteger nextTag = textField.tag + 1; + + UIResponder* nextResponder = [self.view viewWithTag:nextTag]; + if (nextResponder) { + [nextResponder becomeFirstResponder]; + } else { + if ([self allRequiredFieldsEntered]) { + if ([textField isFirstResponder]) + [textField resignFirstResponder]; + + [self saveAction:nil]; + } + } + return NO; +} + + +@end + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITGZIP.h b/submodules/HockeySDK-iOS/Classes/BITGZIP.h new file mode 100644 index 0000000000..dba000867f --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITGZIP.h @@ -0,0 +1,46 @@ +// +// GZIP.h +// +// Version 1.0.3 +// +// Created by Nick Lockwood on 03/06/2012. +// Copyright (C) 2012 Charcoal Design +// +// Distributed under the permissive zlib License +// Get the latest version from here: +// +// https://github.com/nicklockwood/GZIP +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#import + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +@interface NSData (BITGZIP) + +- (nullable NSData *)bit_gzippedDataWithCompressionLevel:(float)level; +- (nullable NSData *)bit_gzippedData; +- (nullable NSData *)bit_gunzippedData; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyAppClient.h b/submodules/HockeySDK-iOS/Classes/BITHockeyAppClient.h new file mode 100644 index 0000000000..2cd677b10c --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyAppClient.h @@ -0,0 +1,95 @@ +/* + * Author: Stephan Diederich + * + * Copyright (c) 2013-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 + +extern NSString * const kBITHockeyAppClientBoundary; + +/** + * Generic Hockey API client + */ +@interface BITHockeyAppClient : NSObject + +/** + * designated initializer + * + * @param baseURL the baseURL of the HockeyApp instance + */ +- (instancetype) initWithBaseURL:(NSURL*) baseURL; + +/** + * baseURL to which relative paths are appended + */ +@property (nonatomic, strong) NSURL *baseURL; + +/** + * creates an NRURLRequest for the given method and path by using + * the internally stored baseURL. + * + * @param method the HTTPMethod to check, must not be nil + * @param params parameters for the request (only supported for GET and POST for now) + * @param path path to append to baseURL. can be nil in which case "/" is appended + * + * @return an NSMutableURLRequest for further configuration + */ +- (NSMutableURLRequest *) requestWithMethod:(NSString*) method + path:(NSString *) path + parameters:(NSDictionary *) params; + +/** + * Access to the internal operation queue + */ +@property (nonatomic, strong) NSOperationQueue *operationQueue; + +#pragma mark - Helpers +/** + * create a post body from the given value, key and boundary. This is a convenience call to + * dataWithPostValue:forKey:contentType:boundary and aimed at NSString-content. + * + * @param value - + * @param key - + * @param boundary - + * + * @return NSData instance configured to be attached on a (post) URLRequest + */ ++ (NSData *)dataWithPostValue:(NSString *)value forKey:(NSString *)key boundary:(NSString *) boundary; + +/** + * create a post body from the given value, key and boundary and content type. + * + * @param value - + * @param key - + *@param contentType - + * @param boundary - + * @param filename - + * + * @return NSData instance configured to be attached on a (post) URLRequest + */ ++ (NSData *)dataWithPostValue:(NSData *)value forKey:(NSString *)key contentType:(NSString *)contentType boundary:(NSString *) boundary filename:(NSString *)filename; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyAppClient.m b/submodules/HockeySDK-iOS/Classes/BITHockeyAppClient.m new file mode 100644 index 0000000000..6da7df4b60 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyAppClient.m @@ -0,0 +1,130 @@ +/* + * Author: Stephan Diederich + * + * Copyright (c) 2013-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 "BITHockeyAppClient.h" + +NSString * const kBITHockeyAppClientBoundary = @"----FOO"; + +@implementation BITHockeyAppClient + +- (instancetype)initWithBaseURL:(NSURL *)baseURL { + self = [super init]; + if ( self ) { + NSParameterAssert(baseURL); + _baseURL = baseURL; + } + return self; +} + +#pragma mark - Networking +- (NSMutableURLRequest *) requestWithMethod:(NSString*) method + path:(NSString *) path + parameters:(NSDictionary *)params { + NSParameterAssert(self.baseURL); + NSParameterAssert(method); + NSParameterAssert(params == nil || [method isEqualToString:@"POST"] || [method isEqualToString:@"GET"]); + path = path ? : @""; + + NSURL *endpoint = [self.baseURL URLByAppendingPathComponent:path]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:endpoint]; + request.HTTPMethod = method; + + if (params) { + if ([method isEqualToString:@"GET"]) { + NSString *absoluteURLString = [endpoint absoluteString]; + //either path already has parameters, or not + NSString *appenderFormat = [path rangeOfString:@"?"].location == NSNotFound ? @"?%@" : @"&%@"; + + endpoint = [NSURL URLWithString:[absoluteURLString stringByAppendingFormat:appenderFormat, + [self.class queryStringFromParameters:params withEncoding:NSUTF8StringEncoding]]]; + [request setURL:endpoint]; + } else { + //TODO: this is crap. Boundary must be the same as the one in appendData + //unify this! + NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", kBITHockeyAppClientBoundary]; + [request setValue:contentType forHTTPHeaderField:@"Content-type"]; + + NSMutableData *postBody = [NSMutableData data]; + [params enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL __unused *stop) { + [postBody appendData:[[self class] dataWithPostValue:value forKey:key boundary:kBITHockeyAppClientBoundary]]; + }]; + + [postBody appendData:(NSData *)[[NSString stringWithFormat:@"--%@--\r\n", kBITHockeyAppClientBoundary] dataUsingEncoding:NSUTF8StringEncoding]]; + + [request setHTTPBody:postBody]; + } + } + + return request; +} + ++ (NSData *)dataWithPostValue:(NSString *)value forKey:(NSString *)key boundary:(NSString *) boundary { + return [self dataWithPostValue:[value dataUsingEncoding:NSUTF8StringEncoding] forKey:key contentType:@"text" boundary:boundary filename:nil]; +} + ++ (NSData *)dataWithPostValue:(NSData *)value forKey:(NSString *)key contentType:(NSString *)contentType boundary:(NSString *) boundary filename:(NSString *)filename { + NSMutableData *postBody = [NSMutableData data]; + + [postBody appendData:(NSData *)[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + + // There's certainly a better way to check if we are supposed to send binary data here. + if (filename){ + [postBody appendData:(NSData *)[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", key, filename] dataUsingEncoding:NSUTF8StringEncoding]]; + [postBody appendData:(NSData *)[[NSString stringWithFormat:@"Content-Type: %@\r\n", contentType] dataUsingEncoding:NSUTF8StringEncoding]]; + [postBody appendData:(NSData *)[[NSString stringWithFormat:@"Content-Transfer-Encoding: binary\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]]; + } else { + [postBody appendData:(NSData *)[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]]; + [postBody appendData:(NSData *)[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", contentType] dataUsingEncoding:NSUTF8StringEncoding]]; + } + + [postBody appendData:value]; + [postBody appendData:(NSData *)[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; + + return postBody; +} + + ++ (NSString *) queryStringFromParameters:(NSDictionary *) params withEncoding:(NSStringEncoding) __unused encoding { + NSMutableString *queryString = [NSMutableString new]; + [params enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSString* value, BOOL __unused *stop) { + NSAssert([key isKindOfClass:[NSString class]], @"Query parameters can only be string-string pairs"); + NSAssert([value isKindOfClass:[NSString class]], @"Query parameters can only be string-string pairs"); + + [queryString appendFormat:queryString.length ? @"&%@=%@" : @"%@=%@", key, value]; + }]; + return queryString; +} + +- (NSOperationQueue *)operationQueue { + if(nil == _operationQueue) { + _operationQueue = [[NSOperationQueue alloc] init]; + _operationQueue.maxConcurrentOperationCount = 1; + } + return _operationQueue; +} + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyAttachment.h b/submodules/HockeySDK-iOS/Classes/BITHockeyAttachment.h new file mode 100644 index 0000000000..ec6cd02df4 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyAttachment.h @@ -0,0 +1,68 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 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 + +/** + Provides support to add binary attachments to crash reports and feedback messages + + This is used by `[BITCrashManagerDelegate attachmentForCrashManager:]`, + `[BITFeedbackComposeViewController prepareWithItems:]` and + `[BITFeedbackManager showFeedbackComposeViewWithPreparedItems:]` + */ +@interface BITHockeyAttachment : NSObject + +/** + The filename the attachment should get + */ +@property (nonatomic, readonly, copy) NSString *filename; + +/** + The attachment data as NSData object + */ +@property (nonatomic, readonly, strong) NSData *hockeyAttachmentData; + +/** + The content type of your data as MIME type + */ +@property (nonatomic, readonly, copy) NSString *contentType; + +/** + Create an BITHockeyAttachment instance with a given filename and NSData object + + @param filename The filename the attachment should get. If nil will get a automatically generated filename + @param hockeyAttachmentData The attachment data as NSData. The instance will be ignore if this is set to nil! + @param contentType The content type of your data as MIME type. If nil will be set to "application/octet-stream" + + @return An instance of BITHockeyAttachment. + */ +- (instancetype)initWithFilename:(NSString *)filename + hockeyAttachmentData:(NSData *)hockeyAttachmentData + contentType:(NSString *)contentType; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyAttachment.m b/submodules/HockeySDK-iOS/Classes/BITHockeyAttachment.m new file mode 100644 index 0000000000..283f267785 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyAttachment.m @@ -0,0 +1,81 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER || HOCKEYSDK_FEATURE_FEEDBACK + +#import "BITHockeyAttachment.h" + +@implementation BITHockeyAttachment + +- (instancetype)initWithFilename:(NSString *)filename + hockeyAttachmentData:(NSData *)hockeyAttachmentData + contentType:(NSString *)contentType +{ + if ((self = [super init])) { + _filename = filename; + + _hockeyAttachmentData = hockeyAttachmentData; + + if (contentType) { + _contentType = contentType; + } else { + _contentType = @"application/octet-stream"; + } + + } + + return self; +} + + +#pragma mark - NSCoder + +- (void)encodeWithCoder:(NSCoder *)encoder { + if (self.filename) { + [encoder encodeObject:self.filename forKey:@"filename"]; + } + if (self.hockeyAttachmentData) { + [encoder encodeObject:self.hockeyAttachmentData forKey:@"data"]; + } + [encoder encodeObject:self.contentType forKey:@"contentType"]; +} + +- (instancetype)initWithCoder:(NSCoder *)decoder { + if ((self = [super init])) { + _filename = [decoder decodeObjectForKey:@"filename"]; + _hockeyAttachmentData = [decoder decodeObjectForKey:@"data"]; + _contentType = [decoder decodeObjectForKey:@"contentType"]; + } + return self; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER || HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyBaseManager.h b/submodules/HockeySDK-iOS/Classes/BITHockeyBaseManager.h new file mode 100644 index 0000000000..20da0b8c18 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyBaseManager.h @@ -0,0 +1,86 @@ +/* + * 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 +#import + + +/** + The internal superclass for all component managers + + */ + +@interface BITHockeyBaseManager : NSObject + +///----------------------------------------------------------------------------- +/// @name Modules +///----------------------------------------------------------------------------- + + +/** + Defines the server URL to send data to or request data from + + By default this is set to the HockeyApp servers and there rarely should be a + need to modify that. + */ +@property (nonatomic, copy) NSString *serverURL; + + +///----------------------------------------------------------------------------- +/// @name User Interface +///----------------------------------------------------------------------------- + +/** + The UIBarStyle of the update user interface navigation bar. + + Default is UIBarStyleBlackOpaque + @see navigationBarTintColor + */ +@property (nonatomic, assign) UIBarStyle barStyle; + +/** + The navigationbar tint color of the update user interface navigation bar. + + The navigationBarTintColor is used by default, you can either overwrite it `navigationBarTintColor` + or define another `barStyle` instead. + + Default is RGB(25, 25, 25) + @see barStyle + */ +@property (nonatomic, strong) UIColor *navigationBarTintColor; + +/** + The UIModalPresentationStyle for showing the update user interface when invoked + with the update alert. + */ +@property (nonatomic, assign) UIModalPresentationStyle modalPresentationStyle; + ++ (void)setPresentAlert:(void (^)(UIAlertController *))presentAlert; ++ (void)setPresentView:(void (^)(UIViewController *))presentView; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyBaseManager.m b/submodules/HockeySDK-iOS/Classes/BITHockeyBaseManager.m new file mode 100644 index 0000000000..03b70b80cf --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyBaseManager.m @@ -0,0 +1,372 @@ +/* + * 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 "HockeySDK.h" +#import "HockeySDKPrivate.h" + +#import "BITHockeyHelper.h" + +#import "BITHockeyBaseManager.h" +#import "BITHockeyBaseManagerPrivate.h" +#if HOCKEYSDK_FEATURE_AUTHENTICATOR || HOCKEYSDK_FEATURE_UPDATES || HOCKEYSDK_FEATURE_FEEDBACK +#import "BITHockeyBaseViewController.h" +#endif + +#import "BITKeychainUtils.h" + +#import +#import +#import + +// We need BIT_UNUSED macro to make sure there aren't any warnings when building +// HockeySDK Distribution scheme. Since several configurations are build in this scheme +// and different features can be turned on and off we can't just use __unused attribute. +#if !defined (HOCKEYSDK_CONFIGURATION_ReleaseCrashOnlyExtensions) +#if HOCKEYSDK_FEATURE_AUTHENTICATOR || HOCKEYSDK_FEATURE_UPDATES || HOCKEYSDK_FEATURE_FEEDBACK +#define BIT_UNUSED +#else +#define BIT_UNUSED __unused +#endif +#endif + +@interface BITHockeyBaseManager () + +@property (nonatomic, strong) UINavigationController *navController; +@property (nonatomic, strong) NSDateFormatter *rfc3339Formatter; + +@end + +static void (^_presentAlert)(UIAlertController *) = nil; +static void (^_presentView)(UIViewController *) = nil; + +@implementation BITHockeyBaseManager + +- (instancetype)init { + if ((self = [super init])) { + _serverURL = BITHOCKEYSDK_URL; + _barStyle = UIBarStyleDefault; + _modalPresentationStyle = UIModalPresentationFormSheet; + + NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + _rfc3339Formatter = [[NSDateFormatter alloc] init]; + [_rfc3339Formatter setLocale:enUSPOSIXLocale]; + [_rfc3339Formatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"]; + [_rfc3339Formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + } + return self; +} + +- (instancetype)initWithAppIdentifier:(NSString *)appIdentifier appEnvironment:(BITEnvironment)environment { + if ((self = [self init])) { + _appIdentifier = appIdentifier; + _appEnvironment = environment; + } + return self; +} + + +#pragma mark - Private + +- (void)reportError:(NSError *)error { + BITHockeyLogError(@"ERROR: %@", [error localizedDescription]); +} + +- (NSString *)encodedAppIdentifier { + return bit_encodeAppIdentifier(self.appIdentifier); +} + +- (NSString *)getDevicePlatform { + 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 *)executableUUID { + const struct mach_header *executableHeader = NULL; + for (uint32_t i = 0; i < _dyld_image_count(); i++) { + const struct mach_header *header = _dyld_get_image_header(i); + if (header->filetype == MH_EXECUTE) { + executableHeader = header; + break; + } + } + + if (!executableHeader) + return @""; + + BOOL is64bit = executableHeader->magic == MH_MAGIC_64 || executableHeader->magic == MH_CIGAM_64; + uintptr_t cursor = (uintptr_t)executableHeader + (is64bit ? sizeof(struct mach_header_64) : sizeof(struct mach_header)); + const struct segment_command *segmentCommand = NULL; + for (uint32_t i = 0; i < executableHeader->ncmds; i++, cursor += segmentCommand->cmdsize) { + segmentCommand = (struct segment_command *)cursor; + if (segmentCommand->cmd == LC_UUID) { + const struct uuid_command *uuidCommand = (const struct uuid_command *)segmentCommand; + const uint8_t *uuid = uuidCommand->uuid; + return [[NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", + uuid[0], uuid[1], uuid[2], uuid[3], + uuid[4], uuid[5], uuid[6], uuid[7], + uuid[8], uuid[9], uuid[10], uuid[11], + uuid[12], uuid[13], uuid[14], uuid[15]] + lowercaseString]; + } + } + + return @""; +} + +#if !defined (HOCKEYSDK_CONFIGURATION_ReleaseCrashOnlyExtensions) +- (UIWindow *)findVisibleWindow { + UIWindow *visibleWindow = [UIApplication sharedApplication].keyWindow; + + if (!(visibleWindow.hidden)) { + return visibleWindow; + } + + // if the rootViewController property (available >= iOS 4.0) of the main window is set, we present the modal view controller on top of the rootViewController + NSArray *windows = [[UIApplication sharedApplication] windows]; + for (UIWindow *window in windows) { + if (!window.hidden && !visibleWindow) { + visibleWindow = window; + } + if ([UIWindow instancesRespondToSelector:@selector(rootViewController)]) { + if (!(window.hidden) && ([window rootViewController])) { + visibleWindow = window; + BITHockeyLogDebug(@"INFO: UIWindow with rootViewController found: %@", visibleWindow); + break; + } + } + } + + return visibleWindow; +} + +/** + * Provide a custom UINavigationController with customized appearance settings + * + * @param viewController The root viewController + * @param modalPresentationStyle The modal presentation style + * + * @return A UINavigationController + */ +- (UINavigationController *)customNavigationControllerWithRootViewController:(UIViewController *)viewController presentationStyle:(UIModalPresentationStyle)modalPresentationStyle { + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:viewController]; + navController.navigationBar.barStyle = self.barStyle; + if (self.navigationBarTintColor) { + navController.navigationBar.tintColor = self.navigationBarTintColor; + } else { + // in case of iOS 7 we overwrite the tint color on the navigation bar + if ([UIWindow instancesRespondToSelector:NSSelectorFromString(@"tintColor")]) { + [navController.navigationBar setTintColor:BIT_RGBCOLOR(0, 122, 255)]; + } + } + navController.modalPresentationStyle = modalPresentationStyle; + + return navController; +} + +- (UIViewController *)visibleWindowRootViewController { + UIViewController *parentViewController = nil; + id strongDelegate = [BITHockeyManager sharedHockeyManager].delegate; + if ([strongDelegate respondsToSelector:@selector(viewControllerForHockeyManager:componentManager:)]) { + parentViewController = [strongDelegate viewControllerForHockeyManager:[BITHockeyManager sharedHockeyManager] componentManager:self]; + } + + UIWindow *visibleWindow = [self findVisibleWindow]; + + if (parentViewController == nil) { + parentViewController = [visibleWindow rootViewController]; + } + + // use topmost modal view + while (parentViewController.presentedViewController) { + parentViewController = parentViewController.presentedViewController; + } + + // special addition to get rootViewController from three20 which has it's own controller handling + if (NSClassFromString(@"TTNavigator")) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + UIViewController *ttParentViewController = nil; + ttParentViewController = [[NSClassFromString(@"TTNavigator") performSelector:(NSSelectorFromString(@"navigator"))] visibleViewController]; + if (ttParentViewController) + parentViewController = ttParentViewController; +#pragma clang diagnostic pop + } + + return parentViewController; +} + +- (void)showAlertController:(UIViewController *)alertController { + + // always execute this on the main thread + dispatch_async(dispatch_get_main_queue(), ^{ + if (_presentAlert) { + _presentAlert(alertController); + } else { + UIViewController *parentViewController = [self visibleWindowRootViewController]; + + // as per documentation this only works if called from within viewWillAppear: or viewDidAppear: + // in tests this also worked fine on iOS 6 and 7 but not on iOS 5 so we are still trying this + if ([parentViewController isKindOfClass:NSClassFromString(@"UIAlertController")] || [parentViewController isBeingPresented]) { + BITHockeyLogWarning(@"WARNING: There is already a view controller being presented onto the parentViewController. Delaying presenting the new view controller by 0.5s."); + [self performSelector:@selector(showAlertController:) withObject:alertController afterDelay:0.5]; + return; + } + + if (parentViewController) { + [parentViewController presentViewController:alertController animated:YES completion:nil]; + } + } + }); +} + +- (void)showView:(UIViewController *)viewController { + if (_presentView) { + _presentView(viewController); + return; + } + + // if we compile Crash only, then BITHockeyBaseViewController is not included + // in the headers and will cause a warning with the modulemap file +#if HOCKEYSDK_FEATURE_AUTHENTICATOR || HOCKEYSDK_FEATURE_UPDATES || HOCKEYSDK_FEATURE_FEEDBACK + UIViewController *parentViewController = [self visibleWindowRootViewController]; + + // as per documentation this only works if called from within viewWillAppear: or viewDidAppear: + // in tests this also worked fine on iOS 6 and 7 but not on iOS 5 so we are still trying this + if ([parentViewController isBeingPresented]) { + BITHockeyLogDebug(@"INFO: There is already a view controller being presented onto the parentViewController. Delaying presenting the new view controller by 0.5s."); + [self performSelector:@selector(showView:) withObject:viewController afterDelay:0.5]; + return; + } + + if (self.navController != nil) self.navController = nil; + + self.navController = [self customNavigationControllerWithRootViewController:viewController presentationStyle:self.modalPresentationStyle]; + + if (parentViewController) { + self.navController.modalTransitionStyle = UIModalTransitionStyleCoverVertical; + + // page sheet for the iPad + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + self.navController.modalPresentationStyle = UIModalPresentationFormSheet; + } + + if ([viewController isKindOfClass:[BITHockeyBaseViewController class]]) + [(BITHockeyBaseViewController *)viewController setModalAnimated:YES]; + + [parentViewController presentViewController:self.navController animated:YES completion:nil]; + } else { + // if not, we add a subview to the window. A bit hacky but should work in most circumstances. + // Also, we don't get a nice animation for free, but hey, this is for beta not production users ;) + UIWindow *visibleWindow = [self findVisibleWindow]; + + BITHockeyLogDebug(@"INFO: No rootViewController found, using UIWindow-approach: %@", visibleWindow); + if ([viewController isKindOfClass:[BITHockeyBaseViewController class]]) + [(BITHockeyBaseViewController *)viewController setModalAnimated:NO]; + [visibleWindow addSubview:self.navController.view]; + } +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR || HOCKEYSDK_FEATURE_UPDATES || HOCKEYSDK_FEATURE_FEEDBACK */ +} +#endif // HOCKEYSDK_CONFIGURATION_ReleaseCrashOnlyExtensions && HOCKEYSDK_CONFIGURATION_RelaseCrashOnlyWatchOS + + +- (BOOL)addStringValueToKeychain:(NSString *)stringValue forKey:(NSString *)key { + if (!key || !stringValue) + return NO; + + NSError *error = nil; + return [BITKeychainUtils storeUsername:key + andPassword:stringValue + forServiceName:bit_keychainHockeySDKServiceName() + updateExisting:YES + error:&error]; +} + +- (BOOL)addStringValueToKeychainForThisDeviceOnly:(NSString *)stringValue forKey:(NSString *)key { + if (!key || !stringValue) + return NO; + + NSError *error = nil; + return [BITKeychainUtils storeUsername:key + andPassword:stringValue + forServiceName:bit_keychainHockeySDKServiceName() + updateExisting:YES + accessibility:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + error:&error]; +} + +- (NSString *)stringValueFromKeychainForKey:(NSString *)key { + if (!key) + return nil; + + NSError *error = nil; + return [BITKeychainUtils getPasswordForUsername:key + andServiceName:bit_keychainHockeySDKServiceName() + error:&error]; +} + +- (BOOL)removeKeyFromKeychain:(NSString *)key { + NSError *error = nil; + return [BITKeychainUtils deleteItemForUsername:key + andServiceName:bit_keychainHockeySDKServiceName() + error:&error]; +} + + +#pragma mark - Manager Control + +- (void)startManager { +} + +#pragma mark - Helpers + +- (NSDate *)parseRFC3339Date:(NSString *)dateString { + NSDate *date = nil; + NSError *error = nil; + if (![self.rfc3339Formatter getObjectValue:&date forString:dateString range:nil error:&error]) { + BITHockeyLogWarning(@"WARNING: Invalid date '%@' string: %@", dateString, error); + } + + return date; +} + ++ (void)setPresentAlert:(void (^)(UIAlertController *))presentAlert { + _presentAlert = [presentAlert copy]; +} + ++ (void)setPresentView:(void (^)(UIViewController *))presentView { + _presentView = [presentView copy]; +} + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyBaseManagerPrivate.h b/submodules/HockeySDK-iOS/Classes/BITHockeyBaseManagerPrivate.h new file mode 100644 index 0000000000..2ddf12d92e --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyBaseManagerPrivate.h @@ -0,0 +1,94 @@ +/* + * 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 +#import +#import "BITHockeyManager.h" + +@class BITHockeyBaseManager; +@class BITHockeyBaseViewController; + +@interface BITHockeyBaseManager() + +@property (nonatomic, copy) NSString *appIdentifier; + +@property (nonatomic, assign, readonly) BITEnvironment appEnvironment; + +- (instancetype)initWithAppIdentifier:(NSString *)appIdentifier appEnvironment:(BITEnvironment)environment; + +- (void)startManager; + +/** + * by default, just logs the message + * + * can be overridden by subclasses to do their own error handling, + * e.g. to show UI + * + * @param error NSError + */ +- (void)reportError:(NSError *)error; + +/** url encoded version of the appIdentifier + + where appIdentifier is either the value this object was initialized with, + or the main bundles CFBundleIdentifier if appIdentifier is nil + */ +- (NSString *)encodedAppIdentifier; + +// device / application helpers +- (NSString *)getDevicePlatform; +- (NSString *)executableUUID; + +#if !defined (HOCKEYSDK_CONFIGURATION_ReleaseCrashOnlyExtensions) +// UI helpers +- (UIWindow *)findVisibleWindow; +- (UINavigationController *)customNavigationControllerWithRootViewController:(UIViewController *)viewController presentationStyle:(UIModalPresentationStyle)presentationStyle; + +/** + * Present an UIAlertController on the visible root UIViewController. + * + * Uses `visibleWindowRootViewController` to find a controller on which to present the UIAlertController on. + * This method is always dispatched on the main queue. + * + * @param alertController The UIAlertController to be presented. + */ +- (void)showAlertController:(UIViewController *)alertController; + +- (void)showView:(UIViewController *)viewController; +#endif + +// Date helpers +- (NSDate *)parseRFC3339Date:(NSString *)dateString; + +// keychain helpers +- (BOOL)addStringValueToKeychain:(NSString *)stringValue forKey:(NSString *)key; +- (BOOL)addStringValueToKeychainForThisDeviceOnly:(NSString *)stringValue forKey:(NSString *)key; +- (NSString *)stringValueFromKeychainForKey:(NSString *)key; +- (BOOL)removeKeyFromKeychain:(NSString *)key; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyBaseViewController.h b/submodules/HockeySDK-iOS/Classes/BITHockeyBaseViewController.h new file mode 100644 index 0000000000..d7701036be --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyBaseViewController.h @@ -0,0 +1,38 @@ +/* + * 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 + +@interface BITHockeyBaseViewController : UITableViewController + +@property (nonatomic, readwrite) BOOL modalAnimated; + +- (instancetype)initWithModalStyle:(BOOL)modal; +- (instancetype)initWithStyle:(UITableViewStyle)style modal:(BOOL)modal; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyBaseViewController.m b/submodules/HockeySDK-iOS/Classes/BITHockeyBaseViewController.m new file mode 100644 index 0000000000..1632c342ce --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyBaseViewController.m @@ -0,0 +1,109 @@ +/* + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR || HOCKEYSDK_FEATURE_UPDATES || HOCKEYSDK_FEATURE_FEEDBACK + +#import "BITHockeyBaseViewController.h" +#import "HockeySDKPrivate.h" + +@interface BITHockeyBaseViewController () + +@property (nonatomic) BOOL modal; + +@end + +@implementation BITHockeyBaseViewController + + +- (instancetype)initWithStyle:(UITableViewStyle)style { + self = [super initWithStyle:style]; + if (self) { + _modalAnimated = YES; + _modal = NO; + } + return self; +} + +- (instancetype)initWithStyle:(UITableViewStyle)style modal:(BOOL)modal { + self = [self initWithStyle:style]; + if (self) { + _modal = modal; + + //might be better in viewDidLoad, but to workaround rdar://12214613 and as it doesn't + //hurt, we do it here + if (_modal) { + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone + target:self + action:@selector(onDismissModal:)]; + } + } + return self; +} + +- (instancetype)initWithModalStyle:(BOOL)modal { + self = [self initWithStyle:UITableViewStylePlain modal:modal]; + return self; +} + + +#pragma mark - View lifecycle + +- (void)onDismissModal:(id) __unused sender { + if (self.modal) { + UIViewController *presentingViewController = [self presentingViewController]; + + // If there is no presenting view controller just remove view + if (presentingViewController && self.modalAnimated) { + [presentingViewController dismissViewControllerAnimated:YES completion:nil]; + } else { + [self.navigationController.view removeFromSuperview]; + } + } else { + [self.navigationController popViewControllerAnimated:YES]; + } +} + + +#pragma mark - Rotation + +-(UIInterfaceOrientationMask)supportedInterfaceOrientations { + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { + return (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscape); + } else { + return UIInterfaceOrientationMaskAll; + } +} + +#pragma mark - Modal presentation + + +@end + +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR || HOCKEYSDK_FEATURE_UPDATES || HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyHelper+Application.h b/submodules/HockeySDK-iOS/Classes/BITHockeyHelper+Application.h new file mode 100644 index 0000000000..e7397ba63a --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyHelper+Application.h @@ -0,0 +1,48 @@ +#import +#import + +#import "BITHockeyHelper.h" +/* + * Workaround for exporting symbols from category object files. + */ +extern NSString *BITHockeyHelperApplicationCategory; + +/** + * App states + */ +typedef NS_ENUM(NSInteger, BITApplicationState) { + + /** + * Application is active. + */ + BITApplicationStateActive = UIApplicationStateActive, + + /** + * Application is inactive. + */ + BITApplicationStateInactive = UIApplicationStateInactive, + + /** + * Application is in background. + */ + BITApplicationStateBackground = UIApplicationStateBackground, + + /** + * Application state can't be determined. + */ + BITApplicationStateUnknown +}; + +@interface BITHockeyHelper (Application) + +/** + * Get current application state. + * + * @return Current state of the application or BITApplicationStateUnknown while the state can't be determined. + * + * @discussion The application state may not be available everywhere. Application extensions doesn't have it for instance, + * in that case the BITApplicationStateUnknown value is returned. + */ ++ (BITApplicationState)applicationState; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyHelper+Application.m b/submodules/HockeySDK-iOS/Classes/BITHockeyHelper+Application.m new file mode 100644 index 0000000000..1a31bc2e9d --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyHelper+Application.m @@ -0,0 +1,43 @@ +#import "BITHockeyHelper+Application.h" + +/* + * Workaround for exporting symbols from category object files. + */ +NSString *BITHockeyHelperApplicationCategory; + +@implementation BITHockeyHelper (Application) + ++ (BITApplicationState)applicationState { + + // App extensions must not access sharedApplication. + if (!bit_isRunningInAppExtension()) { + + __block BITApplicationState state; + dispatch_block_t block = ^{ + state = (BITApplicationState)[[self class] sharedAppState]; + }; + + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_sync(dispatch_get_main_queue(), block); + } + + return state; + } + return BITApplicationStateUnknown; +} + ++ (UIApplication *)sharedApplication { + + // Compute selector at runtime for more discretion. + SEL sharedAppSel = NSSelectorFromString(@"sharedApplication"); + return ((UIApplication * (*)(id, SEL))[[UIApplication class] methodForSelector:sharedAppSel])([UIApplication class], + sharedAppSel); +} + ++ (UIApplicationState)sharedAppState { + return [[[[self class] sharedApplication] valueForKey:@"applicationState"] longValue]; +} + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyHelper.h b/submodules/HockeySDK-iOS/Classes/BITHockeyHelper.h new file mode 100644 index 0000000000..e25f0d4a76 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyHelper.h @@ -0,0 +1,109 @@ +/* + * 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 +#import +#import "HockeySDKEnums.h" + +@interface BITHockeyHelper : NSObject + +FOUNDATION_EXPORT NSString *const kBITExcludeApplicationSupportFromBackup; + ++ (BOOL)isURLSessionSupported; + +/* + * Checks if the privacy description for iOS 10+ has been set in info plist. + * @return YES for < iOS 10. YES/NO in iOS 10+ if NSPhotoLibraryUsageDescription is present in the app's Info.plist. + */ ++ (BOOL)isPhotoAccessPossible; + +@end + +NSString *bit_settingsDir(void); + +BOOL bit_validateEmail(NSString *email); +NSString *bit_keychainHockeySDKServiceName(void); + +/* Fix bug where Application Support was excluded from backup. */ +void bit_fixBackupAttributeForURL(NSURL *directoryURL); + +NSComparisonResult bit_versionCompare(NSString *stringA, NSString *stringB); +NSString *bit_mainBundleIdentifier(void); +NSString *bit_encodeAppIdentifier(NSString *inputString); +NSString *bit_appIdentifierToGuid(NSString *appIdentifier); +NSString *bit_appName(NSString *placeHolderString); +NSString *bit_UUID(void); +NSString *bit_appAnonID(BOOL forceNewAnonID); +BOOL bit_isPreiOS10Environment(void); +BOOL bit_isAppStoreReceiptSandbox(void); +BOOL bit_hasEmbeddedMobileProvision(void); +BITEnvironment bit_currentAppEnvironment(void); +BOOL bit_isRunningInAppExtension(void); + +/** + * Check if the debugger is attached + * + * Taken from https://github.com/plausiblelabs/plcrashreporter/blob/2dd862ce049e6f43feb355308dfc710f3af54c4d/Source/Crash%20Demo/main.m#L96 + * + * @return `YES` if the debugger is attached to the current process, `NO` otherwise + */ +BOOL bit_isDebuggerAttached(void); + +/* NSString helpers */ +NSString *bit_URLEncodedString(NSString *inputString); + +/* Context helpers */ +NSString *bit_utcDateString(NSDate *date); +NSString *bit_devicePlatform(void); +NSString *bit_devicePlatform(void); +NSString *bit_deviceType(void); +NSString *bit_osVersionBuild(void); +NSString *bit_osName(void); +NSString *bit_deviceLocale(void); +NSString *bit_deviceLanguage(void); +NSString *bit_screenSize(void); +NSString *bit_sdkVersion(void); +NSString *bit_appVersion(void); + +#if !defined (HOCKEYSDK_CONFIGURATION_ReleaseCrashOnly) && !defined (HOCKEYSDK_CONFIGURATION_ReleaseCrashOnlyExtensions) +/* AppIcon helper */ +NSString *bit_validAppIconStringFromIcons(NSBundle *resourceBundle, NSArray *icons); +NSString *bit_validAppIconFilename(NSBundle *bundle, NSBundle *resourceBundle); + +/* UIImage helpers */ +UIImage *bit_roundedCornerImage(UIImage *inputImage, CGFloat cornerSize, NSInteger borderSize); +UIImage *bit_imageToFitSize(UIImage *inputImage, CGSize fitSize, BOOL honorScaleFactor); +UIImage *bit_reflectedImageWithHeight(UIImage *inputImage, NSUInteger height, CGFloat fromAlpha, CGFloat toAlpha); + +UIImage *bit_newWithContentsOfResolutionIndependentFile(NSString * path); +UIImage *bit_imageWithContentsOfResolutionIndependentFile(NSString * path); +UIImage *bit_imageNamed(NSString *imageName, NSString *bundleName); +UIImage *bit_screenshot(void); +UIImage *bit_appIcon(void); + +#endif diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyHelper.m b/submodules/HockeySDK-iOS/Classes/BITHockeyHelper.m new file mode 100644 index 0000000000..c4d6a1cca4 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyHelper.m @@ -0,0 +1,1044 @@ +/* + * 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 */ diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyLogger.h b/submodules/HockeySDK-iOS/Classes/BITHockeyLogger.h new file mode 100644 index 0000000000..83a7169673 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyLogger.h @@ -0,0 +1,21 @@ +// Adapted from 0xced’s post at http://stackoverflow.com/questions/34732814/how-should-i-handle-logs-in-an-objective-c-library/34732815#34732815 + +#import +#import "HockeySDKEnums.h" + +#define BITHockeyLog(_level, _message) [BITHockeyLogger logMessage:_message level:_level file:__FILE__ function:__PRETTY_FUNCTION__ line:__LINE__] + +#define BITHockeyLogError(format, ...) BITHockeyLog(BITLogLevelError, (^{ return [NSString stringWithFormat:(format), ##__VA_ARGS__]; })) +#define BITHockeyLogWarning(format, ...) BITHockeyLog(BITLogLevelWarning, (^{ return [NSString stringWithFormat:(format), ##__VA_ARGS__]; })) +#define BITHockeyLogDebug(format, ...) BITHockeyLog(BITLogLevelDebug, (^{ return [NSString stringWithFormat:(format), ##__VA_ARGS__]; })) +#define BITHockeyLogVerbose(format, ...) BITHockeyLog(BITLogLevelVerbose, (^{ return [NSString stringWithFormat:(format), ##__VA_ARGS__]; })) + +@interface BITHockeyLogger : NSObject + ++ (BITLogLevel)currentLogLevel; ++ (void)setCurrentLogLevel:(BITLogLevel)currentLogLevel; ++ (void)setLogHandler:(BITLogHandler)logHandler; + ++ (void)logMessage:(BITLogMessageProvider)messageProvider level:(BITLogLevel)loglevel file:(const char *)file function:(const char *)function line:(uint)line; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyLogger.m b/submodules/HockeySDK-iOS/Classes/BITHockeyLogger.m new file mode 100644 index 0000000000..1a39c23dd3 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyLogger.m @@ -0,0 +1,42 @@ +#import "BITHockeyLoggerPrivate.h" +#import "HockeySDK.h" + +@implementation BITHockeyLogger + +static BITLogLevel _currentLogLevel = BITLogLevelWarning; +static BITLogHandler currentLogHandler; + +BITLogHandler const defaultLogHandler = ^(BITLogMessageProvider messageProvider, BITLogLevel logLevel, const char __unused *file, const char *function, uint line) { + if (messageProvider) { + if (_currentLogLevel < logLevel) { + return; + } + NSString *functionString = [NSString stringWithUTF8String:function]; + NSLog((@"[HockeySDK] %@/%d %@"), functionString, line, messageProvider()); + } +}; + + ++ (void)initialize { + currentLogHandler = defaultLogHandler; +} + ++ (BITLogLevel)currentLogLevel { + return _currentLogLevel; +} + ++ (void)setCurrentLogLevel:(BITLogLevel)currentLogLevel { + _currentLogLevel = currentLogLevel; +} + ++ (void)setLogHandler:(BITLogHandler)logHandler { + currentLogHandler = logHandler; +} + ++ (void)logMessage:(BITLogMessageProvider)messageProvider level:(BITLogLevel)loglevel file:(const char *)file function:(const char *)function line:(uint)line { + if (currentLogHandler) { + currentLogHandler(messageProvider, loglevel, file, function, line); + } +} + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyLoggerPrivate.h b/submodules/HockeySDK-iOS/Classes/BITHockeyLoggerPrivate.h new file mode 100644 index 0000000000..279cd11384 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyLoggerPrivate.h @@ -0,0 +1,3 @@ +#import "BITHockeyLogger.h" + +FOUNDATION_EXPORT BITLogHandler const defaultLogHandler; diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyManager.h b/submodules/HockeySDK-iOS/Classes/BITHockeyManager.h new file mode 100644 index 0000000000..1060a1f298 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyManager.h @@ -0,0 +1,618 @@ +/* + * Author: Andreas Linde + * Kent Sutherland + * + * 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 +#import + +#import "HockeySDKFeatureConfig.h" +#import "HockeySDKEnums.h" + +@protocol BITHockeyManagerDelegate; + +@class BITHockeyBaseManager; +#if HOCKEYSDK_FEATURE_CRASH_REPORTER +@class BITCrashManager; +#endif +#if HOCKEYSDK_FEATURE_UPDATES +@class BITUpdateManager; +#endif +#if HOCKEYSDK_FEATURE_STORE_UPDATES +@class BITStoreUpdateManager; +#endif +#if HOCKEYSDK_FEATURE_FEEDBACK +@class BITFeedbackManager; +#endif +#if HOCKEYSDK_FEATURE_AUTHENTICATOR +@class BITAuthenticator; +#endif +#if HOCKEYSDK_FEATURE_METRICS +@class BITMetricsManager; +#endif + +/** + The HockeySDK manager. Responsible for setup and management of all components + + This is the principal SDK class. It represents the entry point for the HockeySDK. The main promises of the class are initializing the SDK modules, providing access to global properties and to all modules. Initialization is divided into several distinct phases: + + 1. Setup the [HockeyApp](http://hockeyapp.net/) app identifier and the optional delegate: This is the least required information on setting up the SDK and using it. It does some simple validation of the app identifier and checks if the app is running from the App Store or not. + 2. Provides access to the SDK modules `BITCrashManager`, `BITUpdateManager`, and `BITFeedbackManager`. This way all modules can be further configured to personal needs, if the defaults don't fit the requirements. + 3. Configure each module. + 4. Start up all modules. + + The SDK is optimized to defer everything possible to a later time while making sure e.g. crashes on startup can also be caught and each module executes other code with a delay some seconds. This ensures that applicationDidFinishLaunching will process as fast as possible and the SDK will not block the startup sequence resulting in a possible kill by the watchdog process. + + All modules do **NOT** show any user interface if the module is not activated or not integrated. + `BITCrashManager`: Shows an alert on startup asking the user if he/she agrees on sending the crash report, if `[BITCrashManager crashManagerStatus]` is set to `BITCrashManagerStatusAlwaysAsk` (default) + `BITUpdateManager`: Is automatically deactivated when the SDK detects it is running from a build distributed via the App Store. Otherwise if it is not deactivated manually, it will show an alert after startup informing the user about a pending update, if one is available. If the user then decides to view the update another screen is presented with further details and an option to install the update. + `BITFeedbackManager`: If this module is deactivated or the user interface is nowhere added into the app, this module will not do anything. It will not fetch the server for data or show any user interface. If it is integrated, activated, and the user already used it to provide feedback, it will show an alert after startup if a new answer has been received from the server with the option to view it. + + Example: + + [[BITHockeyManager sharedHockeyManager] + configureWithIdentifier:@"" + delegate:nil]; + [[BITHockeyManager sharedHockeyManager] startManager]; + + @warning The SDK is **NOT** thread safe and has to be set up on the main thread! + + @warning Most properties of all components require to be set **BEFORE** calling`startManager`! + + */ + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +@interface BITHockeyManager: NSObject + +#pragma mark - Public Methods + +///----------------------------------------------------------------------------- +/// @name Initialization +///----------------------------------------------------------------------------- + +/** + Returns a shared BITHockeyManager object + + @return A singleton BITHockeyManager instance ready use + */ ++ (BITHockeyManager *)sharedHockeyManager; + + +/** + Initializes the manager with a particular app identifier + + Initialize the manager with a HockeyApp app identifier. + + [[BITHockeyManager sharedHockeyManager] + configureWithIdentifier:@""]; + + @see configureWithIdentifier:delegate: + @see configureWithBetaIdentifier:liveIdentifier:delegate: + @see startManager + @param appIdentifier The app identifier that should be used. + */ +- (void)configureWithIdentifier:(NSString *)appIdentifier; + + +/** + Initializes the manager with a particular app identifier and delegate + + Initialize the manager with a HockeyApp app identifier and assign the class that + implements the optional protocols `BITHockeyManagerDelegate`, `BITCrashManagerDelegate` or + `BITUpdateManagerDelegate`. + + [[BITHockeyManager sharedHockeyManager] + configureWithIdentifier:@"" + delegate:nil]; + + @see configureWithIdentifier: + @see configureWithBetaIdentifier:liveIdentifier:delegate: + @see startManager + @see BITHockeyManagerDelegate + @see BITCrashManagerDelegate + @see BITUpdateManagerDelegate + @see BITFeedbackManagerDelegate + @param appIdentifier The app identifier that should be used. + @param delegate `nil` or the class implementing the option protocols + */ +- (void)configureWithIdentifier:(NSString *)appIdentifier delegate:(nullable id)delegate; + + +/** + Initializes the manager with an app identifier for beta, one for live usage and delegate + + Initialize the manager with different HockeyApp app identifiers for beta and live usage. + All modules will automatically detect if the app is running in the App Store and use + the live app identifier for that. In all other cases it will use the beta app identifier. + And also assign the class that implements the optional protocols `BITHockeyManagerDelegate`, + `BITCrashManagerDelegate` or `BITUpdateManagerDelegate` + + [[BITHockeyManager sharedHockeyManager] + configureWithBetaIdentifier:@"" + liveIdentifier:@"" + delegate:nil]; + + We recommend using one app entry on HockeyApp for your beta versions and another one for + your live versions. The reason is that you will have way more beta versions than live + versions, but on the other side get way more crash reports on the live version. Separating + them into two different app entries makes it easier to work with the data. In addition + you will likely end up having the same version number for a beta and live version which + would mix different data into the same version. Also the live version does not require + you to upload any IPA files, uploading only the dSYM package for crash reporting is + just fine. + + @see configureWithIdentifier: + @see configureWithIdentifier:delegate: + @see startManager + @see BITHockeyManagerDelegate + @see BITCrashManagerDelegate + @see BITUpdateManagerDelegate + @see BITFeedbackManagerDelegate + @param betaIdentifier The app identifier for the _non_ app store (beta) configurations + @param liveIdentifier The app identifier for the app store configurations. + @param delegate `nil` or the class implementing the optional protocols + */ +- (void)configureWithBetaIdentifier:(NSString *)betaIdentifier liveIdentifier:(NSString *)liveIdentifier delegate:(nullable id)delegate; + + +/** + Starts the manager and runs all modules + + Call this after configuring the manager and setting up all modules. + + @see configureWithIdentifier:delegate: + @see configureWithBetaIdentifier:liveIdentifier:delegate: + */ +- (void)startManager; + +#pragma mark - Public Properties + +///----------------------------------------------------------------------------- +/// @name Modules +///----------------------------------------------------------------------------- + + +/** + Set the delegate + + Defines the class that implements the optional protocol `BITHockeyManagerDelegate`. + + The delegate will automatically be propagated to all components. There is no need to set the delegate + for each component individually. + + @warning This property needs to be set before calling `startManager` + + @see BITHockeyManagerDelegate + @see BITCrashManagerDelegate + @see BITUpdateManagerDelegate + @see BITFeedbackManagerDelegate + */ +@property (nonatomic, weak, nullable) id delegate; + + +/** + Defines the server URL to send data to or request data from + + By default this is set to the HockeyApp servers and there rarely should be a + need to modify that. + Please be aware that the URL for `BITMetricsManager` needs to be set separately + as this class uses a different endpoint! + + @warning This property needs to be set before calling `startManager` + */ +@property (nonatomic, copy) NSString *serverURL; + + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER + +/** + Reference to the initialized BITCrashManager module + + Returns the BITCrashManager instance initialized by BITHockeyManager + + @see configureWithIdentifier:delegate: + @see configureWithBetaIdentifier:liveIdentifier:delegate: + @see startManager + @see disableCrashManager + */ +@property (nonatomic, strong, readonly) BITCrashManager *crashManager; + + +/** + Flag the determines whether the Crash Manager should be disabled + + If this flag is enabled, then crash reporting is disabled and no crashes will + be send. + + Please note that the Crash Manager instance will be initialized anyway, but crash report + handling (signal and uncaught exception handlers) will **not** be registered. + + @warning This property needs to be set before calling `startManager` + + *Default*: _NO_ + @see crashManager + */ +@property (nonatomic, getter = isCrashManagerDisabled) BOOL disableCrashManager; + +#endif + + +#if HOCKEYSDK_FEATURE_UPDATES + +/** + Reference to the initialized BITUpdateManager module + + Returns the BITUpdateManager instance initialized by BITHockeyManager + + @see configureWithIdentifier:delegate: + @see configureWithBetaIdentifier:liveIdentifier:delegate: + @see startManager + @see disableUpdateManager + */ +@property (nonatomic, strong, readonly) BITUpdateManager *updateManager; + + +/** + Flag the determines whether the Update Manager should be disabled + + If this flag is enabled, then checking for updates and submitting beta usage + analytics will be turned off! + + Please note that the Update Manager instance will be initialized anyway! + + @warning This property needs to be set before calling `startManager` + + *Default*: _NO_ + @see updateManager + */ +@property (nonatomic, getter = isUpdateManagerDisabled) BOOL disableUpdateManager; + +#endif + + +#if HOCKEYSDK_FEATURE_STORE_UPDATES + +/** + Reference to the initialized BITStoreUpdateManager module + + Returns the BITStoreUpdateManager instance initialized by BITHockeyManager + + @see configureWithIdentifier:delegate: + @see configureWithBetaIdentifier:liveIdentifier:delegate: + @see startManager + @see enableStoreUpdateManager + */ +@property (nonatomic, strong, readonly) BITStoreUpdateManager *storeUpdateManager; + + +/** + Flag the determines whether the App Store Update Manager should be enabled + + If this flag is enabled, then checking for updates when the app runs from the + app store will be turned on! + + Please note that the Store Update Manager instance will be initialized anyway! + + @warning This property needs to be set before calling `startManager` + + *Default*: _NO_ + @see storeUpdateManager + */ +@property (nonatomic, getter = isStoreUpdateManagerEnabled) BOOL enableStoreUpdateManager; + +#endif + + +#if HOCKEYSDK_FEATURE_FEEDBACK + +/** + Reference to the initialized BITFeedbackManager module + + Returns the BITFeedbackManager instance initialized by BITHockeyManager + + @see configureWithIdentifier:delegate: + @see configureWithBetaIdentifier:liveIdentifier:delegate: + @see startManager + @see disableFeedbackManager + */ +@property (nonatomic, strong, readonly) BITFeedbackManager *feedbackManager; + + +/** + Flag the determines whether the Feedback Manager should be disabled + + If this flag is enabled, then letting the user give feedback and + get responses will be turned off! + + Please note that the Feedback Manager instance will be initialized anyway! + + @warning This property needs to be set before calling `startManager` + + *Default*: _NO_ + @see feedbackManager + */ +@property (nonatomic, getter = isFeedbackManagerDisabled) BOOL disableFeedbackManager; + +#endif + + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + +/** + Reference to the initialized BITAuthenticator module + + Returns the BITAuthenticator instance initialized by BITHockeyManager + + @see configureWithIdentifier:delegate: + @see configureWithBetaIdentifier:liveIdentifier:delegate: + @see startManager + */ +@property (nonatomic, strong, readonly) BITAuthenticator *authenticator; + +#endif + +#if HOCKEYSDK_FEATURE_METRICS + +/** + Reference to the initialized BITMetricsManager module + + Returns the BITMetricsManager instance initialized by BITHockeyManager + */ +@property (nonatomic, strong, readonly) BITMetricsManager *metricsManager; + +/** + Flag the determines whether the BITMetricsManager should be disabled + + If this flag is enabled, then sending metrics data such as sessions and users + will be turned off! + + Please note that the BITMetricsManager instance will be initialized anyway! + + *Default*: _NO_ + @see metricsManager + */ +@property (nonatomic, getter = isMetricsManagerDisabled) BOOL disableMetricsManager; + +#endif + +///----------------------------------------------------------------------------- +/// @name Environment +///----------------------------------------------------------------------------- + + +/** + Enum that indicates what kind of environment the application is installed and running in. + + This property can be used to disable or enable specific funtionality + only when specific conditions are met. + That could mean for example, to only enable debug UI elements + when the app has been installed over HockeyApp but not in the AppStore. + + The underlying enum type at the moment only specifies values for the AppStore, + TestFlight and Other. Other summarizes several different distribution methods + and we might define additional specifc values for other environments in the future. + + @see BITEnvironment + */ +@property (nonatomic, readonly) BITEnvironment appEnvironment; + + +/** + Returns the app installation specific anonymous UUID + + The value returned by this method is unique and persisted per app installation + in the keychain. It is also being used in crash reports as `CrashReporter Key` + and internally when sending crash reports and feedback messages. + + This is not identical to the `[ASIdentifierManager advertisingIdentifier]` or + the `[UIDevice identifierForVendor]`! + */ +@property (nonatomic, readonly, copy) NSString *installString; + + +/** + Disable tracking the installation of an app on a device + + This will cause the app to generate a new `installString` value every time the + app is cold started. + + This property is only considered in App Store Environment, since it would otherwise + affect the `BITUpdateManager` and `BITAuthenticator` functionalities! + + @warning This property needs to be set before calling `startManager` + + *Default*: _NO_ + */ +@property (nonatomic, getter=isInstallTrackingDisabled) BOOL disableInstallTracking; + +///----------------------------------------------------------------------------- +/// @name Debug Logging +///----------------------------------------------------------------------------- + +/** + This property is used indicate the amount of verboseness and severity for which + you want to see log messages in the console. + */ +@property (nonatomic, assign) BITLogLevel logLevel; + +/** + Flag that determines whether additional logging output should be generated + by the manager and all modules. + + This is ignored if the app is running in the App Store and reverts to the + default value in that case. + + @warning This property needs to be set before calling `startManager` + + *Default*: _NO_ + */ +@property (nonatomic, assign, getter=isDebugLogEnabled) BOOL debugLogEnabled DEPRECATED_MSG_ATTRIBUTE("Use logLevel instead!"); + +/** + Set a custom block that handles all the log messages that are emitted from the SDK. + + You can use this to reroute the messages that would normally be logged by `NSLog();` + to your own custom logging framework. + + An example of how to do this with NSLogger: + + ``` + [[BITHockeyManager sharedHockeyManager] setLogHandler:^(BITLogMessageProvider messageProvider, BITLogLevel logLevel, const char *file, const char *function, uint line) { + LogMessageRawF(file, (int)line, function, @"HockeySDK", (int)logLevel-1, messageProvider()); + }]; + ``` + + or with CocoaLumberjack: + + ``` + [[BITHockeyManager sharedHockeyManager] setLogHandler:^(BITLogMessageProvider messageProvider, BITLogLevel logLevel, const char *file, const char *function, uint line) { + [DDLog log:YES message:messageProvider() level:ddLogLevel flag:(DDLogFlag)(1 << (logLevel-1)) context:<#CocoaLumberjackContext#> file:file function:function line:line tag:nil]; + }]; + ``` + + @param logHandler The block of type BITLogHandler that will process all logged messages. + */ +- (void)setLogHandler:(BITLogHandler)logHandler; + + +///----------------------------------------------------------------------------- +/// @name Integration test +///----------------------------------------------------------------------------- + +/** + Pings the server with the HockeyApp app identifiers used for initialization + + Call this method once for debugging purposes to test if your SDK setup code + reaches the server successfully. + + Once invoked, check the apps page on HockeyApp for a verification. + + If you setup the SDK with a beta and live identifier, a call to both app IDs will be done. + + This call is ignored if the app is running in the App Store!. + */ +- (void)testIdentifier; + + +///----------------------------------------------------------------------------- +/// @name Additional meta data +///----------------------------------------------------------------------------- + +/** Set the userid that should used in the SDK components + + Right now this is used by the `BITCrashManager` to attach to a crash report. + `BITFeedbackManager` uses it too for assigning the user to a discussion thread. + + The value can be set at any time and will be stored in the keychain on the current + device only! To delete the value from the keychain set the value to `nil`. + + This property is optional and can be used as an alternative to the delegate. If you + want to define specific data for each component, use the delegate instead which does + overwrite the values set by this property. + + @warning When returning a non nil value, crash reports are not anonymous any more + and the crash alerts will not show the word "anonymous"! + + @warning This property needs to be set before calling `startManager` to be considered + for being added to crash reports as meta data. + + @see userName + @see userEmail + @see `[BITHockeyManagerDelegate userIDForHockeyManager:componentManager:]` + */ +@property (nonatomic, copy, nullable) NSString *userID; + + +/** Set the user name that should used in the SDK components + + Right now this is used by the `BITCrashManager` to attach to a crash report. + `BITFeedbackManager` uses it too for assigning the user to a discussion thread. + + The value can be set at any time and will be stored in the keychain on the current + device only! To delete the value from the keychain set the value to `nil`. + + This property is optional and can be used as an alternative to the delegate. If you + want to define specific data for each component, use the delegate instead which does + overwrite the values set by this property. + + @warning When returning a non nil value, crash reports are not anonymous any more + and the crash alerts will not show the word "anonymous"! + + @warning This property needs to be set before calling `startManager` to be considered + for being added to crash reports as meta data. + + @see userID + @see userEmail + @see `[BITHockeyManagerDelegate userNameForHockeyManager:componentManager:]` + */ +@property (nonatomic, copy, nullable) NSString *userName; + + +/** Set the users email address that should used in the SDK components + + Right now this is used by the `BITCrashManager` to attach to a crash report. + `BITFeedbackManager` uses it too for assigning the user to a discussion thread. + + The value can be set at any time and will be stored in the keychain on the current + device only! To delete the value from the keychain set the value to `nil`. + + This property is optional and can be used as an alternative to the delegate. If you + want to define specific data for each component, use the delegate instead which does + overwrite the values set by this property. + + @warning When returning a non nil value, crash reports are not anonymous any more + and the crash alerts will not show the word "anonymous"! + + @warning This property needs to be set before calling `startManager` to be considered + for being added to crash reports as meta data. + + @see userID + @see userName + @see [BITHockeyManagerDelegate userEmailForHockeyManager:componentManager:] + */ +@property (nonatomic, copy, nullable) NSString *userEmail; + + +///----------------------------------------------------------------------------- +/// @name SDK meta data +///----------------------------------------------------------------------------- + +/** + Returns the SDK Version (CFBundleShortVersionString). + */ +- (NSString *)version; + +/** + Returns the SDK Build (CFBundleVersion) as a string. + */ +- (NSString *)build; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyManager.m b/submodules/HockeySDK-iOS/Classes/BITHockeyManager.m new file mode 100644 index 0000000000..cc174a996e --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyManager.m @@ -0,0 +1,756 @@ +/* + * Author: Andreas Linde + * Kent Sutherland + * + * 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 "HockeySDK.h" +#import "HockeySDKPrivate.h" + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER || HOCKEYSDK_FEATURE_FEEDBACK || HOCKEYSDK_FEATURE_UPDATES || HOCKEYSDK_FEATURE_AUTHENTICATOR || HOCKEYSDK_FEATURE_STORE_UPDATES || HOCKEYSDK_FEATURE_METRICS +#import "BITHockeyBaseManagerPrivate.h" +#endif + +#import "BITHockeyHelper.h" +#import "BITHockeyAppClient.h" +#import "BITKeychainUtils.h" + +#include + +typedef struct { + uint8_t info_version; + const char hockey_version[16]; + const char hockey_build[16]; +} bitstadium_info_t; + +static bitstadium_info_t bitstadium_library_info __attribute__((section("__TEXT,__bit_hockey,regular,no_dead_strip"))) = { + .info_version = 1, + .hockey_version = BITHOCKEY_C_VERSION, + .hockey_build = BITHOCKEY_C_BUILD +}; + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER +#import "BITCrashManagerPrivate.h" +#endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ + +#if HOCKEYSDK_FEATURE_UPDATES +#import "BITUpdateManagerPrivate.h" +#endif /* HOCKEYSDK_FEATURE_UPDATES */ + +#if HOCKEYSDK_FEATURE_STORE_UPDATES +#import "BITStoreUpdateManagerPrivate.h" +#endif /* HOCKEYSDK_FEATURE_STORE_UPDATES */ + +#if HOCKEYSDK_FEATURE_FEEDBACK +#import "BITFeedbackManagerPrivate.h" +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR +#import "BITAuthenticator_Private.h" +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ + +#if HOCKEYSDK_FEATURE_METRICS +#import "BITMetricsManagerPrivate.h" +#import "BITCategoryContainer.h" +#endif /* HOCKEYSDK_FEATURE_METRICS */ + +@interface BITHockeyManager () + +- (BOOL)shouldUseLiveIdentifier; + +@property (nonatomic, copy) NSString *appIdentifier; +@property (nonatomic, copy) NSString *liveIdentifier; +@property (nonatomic) BOOL validAppIdentifier; +@property (nonatomic) BOOL startManagerIsInvoked; +@property (nonatomic) BOOL startUpdateManagerIsInvoked; +@property (nonatomic) BOOL managersInitialized; +@property (nonatomic, strong) BITHockeyAppClient *hockeyAppClient; + +// Redeclare BITHockeyManager properties with readwrite attribute. +@property (nonatomic, readwrite, copy) NSString *installString; + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER +@property (nonatomic, strong, readwrite) BITCrashManager *crashManager; +#endif + +#if HOCKEYSDK_FEATURE_UPDATES +@property (nonatomic, strong, readwrite) BITUpdateManager *updateManager; +#endif + +#if HOCKEYSDK_FEATURE_STORE_UPDATES +@property (nonatomic, strong, readwrite) BITStoreUpdateManager *storeUpdateManager; +#endif + +#if HOCKEYSDK_FEATURE_FEEDBACK +@property (nonatomic, strong, readwrite) BITFeedbackManager *feedbackManager; +#endif + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR +@property (nonatomic, strong, readwrite) BITAuthenticator *authenticator; +#endif + +#if HOCKEYSDK_FEATURE_METRICS +@property (nonatomic, strong, readwrite) BITMetricsManager *metricsManager; +#endif + + +@end + + +@implementation BITHockeyManager + +#pragma mark - Private Class Methods + +- (BOOL)checkValidityOfAppIdentifier:(NSString *)identifier { + BOOL result = NO; + + if (identifier) { + NSCharacterSet *hexSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdef"]; + NSCharacterSet *inStringSet = [NSCharacterSet characterSetWithCharactersInString:identifier]; + result = ([identifier length] == 32) && ([hexSet isSupersetOfSet:inStringSet]); + } + + return result; +} + +- (void)logInvalidIdentifier:(NSString *)environment { + if (self.appEnvironment != BITEnvironmentAppStore) { + if ([environment isEqualToString:@"liveIdentifier"]) { + BITHockeyLogWarning(@"[HockeySDK] WARNING: The liveIdentifier is invalid! The SDK will be disabled when deployed to the App Store without setting a valid app identifier!"); + } else { + BITHockeyLogError(@"[HockeySDK] ERROR: The %@ is invalid! Please use the HockeyApp app identifier you find on the apps website on HockeyApp! The SDK is disabled!", environment); + } + } +} + +#pragma mark - Public Class Methods + ++ (BITHockeyManager *)sharedHockeyManager { + static BITHockeyManager *sharedInstance = nil; + static dispatch_once_t pred; + + dispatch_once(&pred, ^{ + sharedInstance = [BITHockeyManager alloc]; + sharedInstance = [sharedInstance init]; + }); + + return sharedInstance; +} + +- (instancetype)init { + if ((self = [super init])) { + _serverURL = BITHOCKEYSDK_URL; + _delegate = nil; + _managersInitialized = NO; + + _hockeyAppClient = nil; + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER + _disableCrashManager = NO; +#endif +#if HOCKEYSDK_FEATURE_METRICS + _disableMetricsManager = NO; +#endif +#if HOCKEYSDK_FEATURE_FEEDBACK + _disableFeedbackManager = NO; +#endif +#if HOCKEYSDK_FEATURE_UPDATES + _disableUpdateManager = NO; +#endif +#if HOCKEYSDK_FEATURE_STORE_UPDATES + _enableStoreUpdateManager = NO; +#endif + + _appEnvironment = bit_currentAppEnvironment(); + _startManagerIsInvoked = NO; + _startUpdateManagerIsInvoked = NO; + + _liveIdentifier = nil; + _installString = bit_appAnonID(NO); + _disableInstallTracking = NO; + + [self performSelector:@selector(validateStartManagerIsInvoked) withObject:nil afterDelay:0.0]; + } + return self; +} + +- (void)dealloc { +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + // start Authenticator + if (self.appEnvironment != BITEnvironmentAppStore) { + [self.authenticator removeObserver:self forKeyPath:@"identified"]; + } +#endif +} + + +#pragma mark - Public Instance Methods (Configuration) + +- (void)configureWithIdentifier:(NSString *)appIdentifier { + self.appIdentifier = [appIdentifier copy]; + + [self initializeModules]; +} + +- (void)configureWithIdentifier:(NSString *)appIdentifier delegate:(id)delegate { + self.delegate = delegate; + self.appIdentifier = [appIdentifier copy]; + + [self initializeModules]; +} + +- (void)configureWithBetaIdentifier:(NSString *)betaIdentifier liveIdentifier:(NSString *)liveIdentifier delegate:(id)delegate { + self.delegate = delegate; + + // check the live identifier now, because otherwise invalid identifier would only be logged when the app is already in the store + if (![self checkValidityOfAppIdentifier:liveIdentifier]) { + [self logInvalidIdentifier:@"liveIdentifier"]; + self.liveIdentifier = [liveIdentifier copy]; + } + + if ([self shouldUseLiveIdentifier]) { + self.appIdentifier = [liveIdentifier copy]; + } + else { + self.appIdentifier = [betaIdentifier copy]; + } + + [self initializeModules]; +} + +- (void)startManager { + if (!self.validAppIdentifier) return; + if (self.startManagerIsInvoked) { + BITHockeyLogWarning(@"[HockeySDK] Warning: startManager should only be invoked once! This call is ignored."); + return; + } + + // Fix bug where Application Support directory was encluded from backup + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *appSupportURL = [[fileManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject]; + bit_fixBackupAttributeForURL(appSupportURL); + + if (![self isSetUpOnMainThread]) return; + + if ((self.appEnvironment == BITEnvironmentAppStore) && [self isInstallTrackingDisabled]) { + self.installString = bit_appAnonID(YES); + } + + BITHockeyLogDebug(@"INFO: Starting HockeyManager"); + self.startManagerIsInvoked = YES; + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER + // start CrashManager + if (![self isCrashManagerDisabled]) { + BITHockeyLogDebug(@"INFO: Start CrashManager"); + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + if (self.authenticator) { + [self.crashManager setInstallationIdentification:[self.authenticator publicInstallationIdentifier]]; + [self.crashManager setInstallationIdentificationType:[self.authenticator identificationType]]; + [self.crashManager setInstallationIdentified:[self.authenticator isIdentified]]; + } +#endif + + [self.crashManager startManager]; + } +#endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ + +#if HOCKEYSDK_FEATURE_METRICS + // start MetricsManager + if (!self.isMetricsManagerDisabled) { + BITHockeyLogDebug(@"INFO: Start MetricsManager"); + [self.metricsManager startManager]; + [BITCategoryContainer activateCategory]; + } +#endif /* HOCKEYSDK_FEATURE_METRICS */ + + // App Extensions can only use BITCrashManager and BITMetricsManager, so ignore all others automatically + if (bit_isRunningInAppExtension()) { + return; + } + +#if HOCKEYSDK_FEATURE_STORE_UPDATES + // start StoreUpdateManager + if ([self isStoreUpdateManagerEnabled]) { + BITHockeyLogDebug(@"INFO: Start StoreUpdateManager"); + if (self.serverURL) { + [self.storeUpdateManager setServerURL:self.serverURL]; + } + [self.storeUpdateManager performSelector:@selector(startManager) withObject:nil afterDelay:0.5]; + } +#endif /* HOCKEYSDK_FEATURE_STORE_UPDATES */ + +#if HOCKEYSDK_FEATURE_FEEDBACK + // start FeedbackManager + if (![self isFeedbackManagerDisabled]) { + BITHockeyLogDebug(@"INFO: Start FeedbackManager"); + if (self.serverURL) { + [self.feedbackManager setServerURL:self.serverURL]; + } + [self.feedbackManager performSelector:@selector(startManager) withObject:nil afterDelay:1.0]; + } +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + // start Authenticator + if (self.appEnvironment != BITEnvironmentAppStore) { + // hook into manager with kvo! + [self.authenticator addObserver:self forKeyPath:@"identified" options:0 context:nil]; + + BITHockeyLogDebug(@"INFO: Start Authenticator"); + if (self.serverURL) { + [self.authenticator setServerURL:self.serverURL]; + } + [self.authenticator performSelector:@selector(startManager) withObject:nil afterDelay:0.5]; + } +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ + +#if HOCKEYSDK_FEATURE_UPDATES + BOOL isIdentified = NO; + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + if (self.appEnvironment != BITEnvironmentAppStore) + isIdentified = [self.authenticator isIdentified]; +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ + + // Setup UpdateManager + if (![self isUpdateManagerDisabled] && isIdentified) { + [self invokeStartUpdateManager]; + } +#endif /* HOCKEYSDK_FEATURE_UPDATES */ +} + +#if HOCKEYSDK_FEATURE_UPDATES +- (void)setDisableUpdateManager:(BOOL)disableUpdateManager { + if (self.updateManager) { + [self.updateManager setDisableUpdateManager:disableUpdateManager]; + } + _disableUpdateManager = disableUpdateManager; +} +#endif /* HOCKEYSDK_FEATURE_UPDATES */ + + +#if HOCKEYSDK_FEATURE_STORE_UPDATES +- (void)setEnableStoreUpdateManager:(BOOL)enableStoreUpdateManager { + if (self.storeUpdateManager) { + [self.storeUpdateManager setEnableStoreUpdateManager:enableStoreUpdateManager]; + } + _enableStoreUpdateManager = enableStoreUpdateManager; +} +#endif /* HOCKEYSDK_FEATURE_STORE_UPDATES */ + + +#if HOCKEYSDK_FEATURE_FEEDBACK +- (void)setDisableFeedbackManager:(BOOL)disableFeedbackManager { + if (self.feedbackManager) { + [self.feedbackManager setDisableFeedbackManager:disableFeedbackManager]; + } + _disableFeedbackManager = disableFeedbackManager; +} +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ + +#if HOCKEYSDK_FEATURE_METRICS +- (void)setDisableMetricsManager:(BOOL)disableMetricsManager { + if (self.metricsManager) { + self.metricsManager.disabled = disableMetricsManager; + } + _disableMetricsManager = disableMetricsManager; +} +#endif /* HOCKEYSDK_FEATURE_METRICS */ + +- (void)setServerURL:(NSString *)aServerURL { + // ensure url ends with a trailing slash + if (![aServerURL hasSuffix:@"/"]) { + aServerURL = [NSString stringWithFormat:@"%@/", aServerURL]; + } + + if (self.serverURL != aServerURL) { + _serverURL = [aServerURL copy]; + + if (self.hockeyAppClient) { + self.hockeyAppClient.baseURL = [NSURL URLWithString:self.serverURL ?: BITHOCKEYSDK_URL]; + } + } +} + + +- (void)setDelegate:(id)delegate { + if (self.appEnvironment != BITEnvironmentAppStore) { + if (self.startManagerIsInvoked) { + BITHockeyLogError(@"[HockeySDK] ERROR: The `delegate` property has to be set before calling [[BITHockeyManager sharedHockeyManager] startManager] !"); + } + } + + if (_delegate != delegate) { + _delegate = delegate; + id currentDelegate = _delegate; + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER + if (self.crashManager) { + self.crashManager.delegate = currentDelegate; + } +#endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ + +#if HOCKEYSDK_FEATURE_UPDATES + if (self.updateManager) { + self.updateManager.delegate = currentDelegate; + } +#endif /* HOCKEYSDK_FEATURE_UPDATES */ + +#if HOCKEYSDK_FEATURE_FEEDBACK + if (self.feedbackManager) { + self.feedbackManager.delegate = currentDelegate; + } +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ + +#if HOCKEYSDK_FEATURE_STORE_UPDATES + if (self.storeUpdateManager) { + self.storeUpdateManager.delegate = currentDelegate; + } +#endif /* HOCKEYSDK_FEATURE_STORE_UPDATES */ + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + if (self.authenticator) { + self.authenticator.delegate = currentDelegate; + } +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ + } +} + +- (void)setDebugLogEnabled:(BOOL)debugLogEnabled { + _debugLogEnabled = debugLogEnabled; + if (debugLogEnabled) { + self.logLevel = BITLogLevelDebug; + } else { + self.logLevel = BITLogLevelWarning; + } +} + +- (BITLogLevel)logLevel { + return BITHockeyLogger.currentLogLevel; +} + +- (void)setLogLevel:(BITLogLevel)logLevel { + BITHockeyLogger.currentLogLevel = logLevel; +} + +- (void)setLogHandler:(BITLogHandler)logHandler { + [BITHockeyLogger setLogHandler:logHandler]; +} + +- (void)modifyKeychainUserValue:(NSString *)value forKey:(NSString *)key { + NSError *error = nil; + BOOL success = YES; + NSString *updateType = @"update"; + + if (value) { + success = [BITKeychainUtils storeUsername:key + andPassword:value + forServiceName:bit_keychainHockeySDKServiceName() + updateExisting:YES + accessibility:kSecAttrAccessibleAlwaysThisDeviceOnly + error:&error]; + } else { + updateType = @"delete"; + if ([BITKeychainUtils getPasswordForUsername:key + andServiceName:bit_keychainHockeySDKServiceName() + error:&error]) { + success = [BITKeychainUtils deleteItemForUsername:key + andServiceName:bit_keychainHockeySDKServiceName() + error:&error]; + } + } + + if (!success) { + NSString *errorDescription = [error description] ?: @""; + BITHockeyLogError(@"ERROR: Couldn't %@ key %@ in the keychain. %@", updateType, key, errorDescription); + } +} + +- (void)setUserID:(NSString *)userID { + // always set it, since nil value will trigger removal of the keychain entry + _userID = userID; + + [self modifyKeychainUserValue:userID forKey:kBITHockeyMetaUserID]; +} + +- (void)setUserName:(NSString *)userName { + // always set it, since nil value will trigger removal of the keychain entry + _userName = userName; + + [self modifyKeychainUserValue:userName forKey:kBITHockeyMetaUserName]; +} + +- (void)setUserEmail:(NSString *)userEmail { + // always set it, since nil value will trigger removal of the keychain entry + _userEmail = userEmail; + + [self modifyKeychainUserValue:userEmail forKey:kBITHockeyMetaUserEmail]; +} + +- (void)testIdentifier { + if (!self.appIdentifier || (self.appEnvironment == BITEnvironmentAppStore)) { + return; + } + + NSDate *now = [NSDate date]; + NSString *timeString = [NSString stringWithFormat:@"%.0f", [now timeIntervalSince1970]]; + [self pingServerForIntegrationStartWorkflowWithTimeString:timeString appIdentifier:self.appIdentifier]; + + if (self.liveIdentifier) { + [self pingServerForIntegrationStartWorkflowWithTimeString:timeString appIdentifier:self.liveIdentifier]; + } +} + + +- (NSString *)version { + return (NSString *)[NSString stringWithUTF8String:bitstadium_library_info.hockey_version]; +} + +- (NSString *)build { + return (NSString *)[NSString stringWithUTF8String:bitstadium_library_info.hockey_build]; +} + + +#pragma mark - KVO + +#if HOCKEYSDK_FEATURE_UPDATES +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *) __unused change context:(void *) __unused context { + if ([keyPath isEqualToString:@"identified"] && + [object valueForKey:@"isIdentified"] ) { + if (self.appEnvironment != BITEnvironmentAppStore) { + BOOL identified = [(NSNumber *)[object valueForKey:@"isIdentified"] boolValue]; + if (identified && ![self isUpdateManagerDisabled]) { + [self invokeStartUpdateManager]; + } + } + } +} +#endif /* HOCKEYSDK_FEATURE_UPDATES */ + + +#pragma mark - Private Instance Methods + +- (BITHockeyAppClient *)hockeyAppClient { + if (!_hockeyAppClient) { + _hockeyAppClient = [[BITHockeyAppClient alloc] initWithBaseURL:[NSURL URLWithString:self.serverURL]]; + } + return _hockeyAppClient; +} + +- (NSString *)integrationFlowTimeString { + NSString *timeString = [[NSBundle mainBundle] objectForInfoDictionaryKey:BITHOCKEY_INTEGRATIONFLOW_TIMESTAMP]; + + return timeString; +} + +- (BOOL)integrationFlowStartedWithTimeString:(NSString *)timeString { + if (timeString == nil || (self.appEnvironment == BITEnvironmentAppStore)) { + return NO; + } + + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + [dateFormatter setLocale:enUSPOSIXLocale]; + [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZ"]; + NSDate *integrationFlowStartDate = [dateFormatter dateFromString:timeString]; + + if (integrationFlowStartDate && [integrationFlowStartDate timeIntervalSince1970] > [[NSDate date] timeIntervalSince1970] - (60 * 10) ) { + return YES; + } + + return NO; +} + +- (void)pingServerForIntegrationStartWorkflowWithTimeString:(NSString *)timeString appIdentifier:(NSString *)appIdentifier { + if (!appIdentifier || (self.appEnvironment == BITEnvironmentAppStore)) { + return; + } + + NSString *integrationPath = [NSString stringWithFormat:@"api/3/apps/%@/integration", bit_encodeAppIdentifier(appIdentifier)]; + + BITHockeyLogDebug(@"INFO: Sending integration workflow ping to %@", integrationPath); + + NSDictionary *params = @{@"timestamp": timeString, + @"sdk": BITHOCKEY_NAME, + @"sdk_version": BITHOCKEY_VERSION, + @"bundle_version": (id)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] + }; + + NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + __block NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; + NSURLRequest *request = [[self hockeyAppClient] requestWithMethod:@"POST" path:integrationPath parameters:params]; + NSURLSessionDataTask *task = [session dataTaskWithRequest:request + completionHandler: ^(NSData * __unused data, NSURLResponse *response, NSError * __unused error) { + [session finishTasksAndInvalidate]; + + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*) response; + [self logPingMessageForStatusCode:httpResponse.statusCode]; + }]; + [task resume]; + +} + +- (void)logPingMessageForStatusCode:(NSInteger)statusCode { + switch (statusCode) { + case 400: + BITHockeyLogError(@"ERROR: App ID not found"); + break; + case 201: + BITHockeyLogDebug(@"INFO: Ping accepted."); + break; + case 200: + BITHockeyLogDebug(@"INFO: Ping accepted. Server already knows."); + break; + default: + BITHockeyLogError(@"ERROR: Unknown error"); + break; + } +} + +- (void)validateStartManagerIsInvoked { + if (self.validAppIdentifier && (self.appEnvironment != BITEnvironmentAppStore)) { + if (!self.startManagerIsInvoked) { + BITHockeyLogError(@"[HockeySDK] ERROR: You did not call [[BITHockeyManager sharedHockeyManager] startManager] to startup the HockeySDK! Please do so after setting up all properties. The SDK is NOT running."); + } + } +} + +#if HOCKEYSDK_FEATURE_UPDATES +- (void)invokeStartUpdateManager { + if (self.startUpdateManagerIsInvoked) return; + + self.startUpdateManagerIsInvoked = YES; + BITHockeyLogDebug(@"INFO: Start UpdateManager"); + if (self.serverURL) { + [self.updateManager setServerURL:self.serverURL]; + } +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + if (self.authenticator) { + [self.updateManager setInstallationIdentification:[self.authenticator installationIdentifier]]; + [self.updateManager setInstallationIdentificationType:[self.authenticator installationIdentifierParameterString]]; + [self.updateManager setInstallationIdentified:[self.authenticator isIdentified]]; + } +#endif + [self.updateManager performSelector:@selector(startManager) withObject:nil afterDelay:0.5]; +} +#endif /* HOCKEYSDK_FEATURE_UPDATES */ + +- (BOOL)isSetUpOnMainThread { + NSString *errorString = @"ERROR: HockeySDK has to be setup on the main thread!"; + + if (!NSThread.isMainThread) { + if (self.appEnvironment == BITEnvironmentAppStore) { + BITHockeyLogError(@"%@", errorString); + } else { + BITHockeyLogError(@"%@", errorString); + NSAssert(NSThread.isMainThread, errorString); + } + + return NO; + } + + return YES; +} + +- (BOOL)shouldUseLiveIdentifier { + BOOL delegateResult = NO; + id currentDelegate = self.delegate; + if ([currentDelegate respondsToSelector:@selector(shouldUseLiveIdentifierForHockeyManager:)]) { + delegateResult = [currentDelegate shouldUseLiveIdentifierForHockeyManager:self]; + } + + return (delegateResult) || (self.appEnvironment == BITEnvironmentAppStore); +} + +- (void)initializeModules { + if (self.managersInitialized) { + BITHockeyLogWarning(@"[HockeySDK] Warning: The SDK should only be initialized once! This call is ignored."); + return; + } + + self.validAppIdentifier = [self checkValidityOfAppIdentifier:self.appIdentifier]; + + if (![self isSetUpOnMainThread]) return; + + self.startManagerIsInvoked = NO; + + if (self.validAppIdentifier) { + id currentDelegate = self.delegate; + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER + BITHockeyLogDebug(@"INFO: Setup CrashManager"); + self.crashManager = [[BITCrashManager alloc] initWithAppIdentifier:self.appIdentifier + appEnvironment:self.appEnvironment + hockeyAppClient:[self hockeyAppClient]]; + self.crashManager.delegate = currentDelegate; +#endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ + +#if HOCKEYSDK_FEATURE_UPDATES + BITHockeyLogDebug(@"INFO: Setup UpdateManager"); + self.updateManager = [[BITUpdateManager alloc] initWithAppIdentifier:self.appIdentifier appEnvironment:self.appEnvironment]; + self.updateManager.delegate = currentDelegate; +#endif /* HOCKEYSDK_FEATURE_UPDATES */ + +#if HOCKEYSDK_FEATURE_STORE_UPDATES + BITHockeyLogDebug(@"INFO: Setup StoreUpdateManager"); + self.storeUpdateManager = [[BITStoreUpdateManager alloc] initWithAppIdentifier:self.appIdentifier appEnvironment:self.appEnvironment]; + self.storeUpdateManager.delegate = currentDelegate; +#endif /* HOCKEYSDK_FEATURE_STORE_UPDATES */ + +#if HOCKEYSDK_FEATURE_FEEDBACK + BITHockeyLogDebug(@"INFO: Setup FeedbackManager"); + self.feedbackManager = [[BITFeedbackManager alloc] initWithAppIdentifier:self.appIdentifier appEnvironment:self.appEnvironment]; + self.feedbackManager.delegate = currentDelegate; +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR + BITHockeyLogDebug(@"INFO: Setup Authenticator"); + self.authenticator = [[BITAuthenticator alloc] initWithAppIdentifier:self.appIdentifier appEnvironment:self.appEnvironment]; + self.authenticator.hockeyAppClient = [self hockeyAppClient]; + self.authenticator.delegate = currentDelegate; +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ + +#if HOCKEYSDK_FEATURE_METRICS + BITHockeyLogDebug(@"INFO: Setup MetricsManager"); + NSString *iKey = bit_appIdentifierToGuid(self.appIdentifier); + self.metricsManager = [[BITMetricsManager alloc] initWithAppIdentifier:iKey appEnvironment:self.appEnvironment]; +#endif /* HOCKEYSDK_FEATURE_METRICS */ + + if (self.appEnvironment != BITEnvironmentAppStore) { + NSString *integrationFlowTime = [self integrationFlowTimeString]; + if (integrationFlowTime && [self integrationFlowStartedWithTimeString:integrationFlowTime]) { + [self pingServerForIntegrationStartWorkflowWithTimeString:integrationFlowTime appIdentifier:self.appIdentifier]; + } + } + self.managersInitialized = YES; + } else { + [self logInvalidIdentifier:@"app identifier"]; + } +} + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITHockeyManagerDelegate.h b/submodules/HockeySDK-iOS/Classes/BITHockeyManagerDelegate.h new file mode 100644 index 0000000000..c4f099b733 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITHockeyManagerDelegate.h @@ -0,0 +1,230 @@ +/* + * 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 +#import "HockeySDKFeatureConfig.h" + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER +#import "BITCrashManagerDelegate.h" +#endif + +#if HOCKEYSDK_FEATURE_UPDATES +#import "BITUpdateManagerDelegate.h" +#endif + +#if HOCKEYSDK_FEATURE_FEEDBACK +#import "BITFeedbackManagerDelegate.h" +#endif + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR +#import "BITAuthenticator.h" +#endif + +@class BITHockeyManager; +@class BITHockeyBaseManager; + +/** + The `BITHockeyManagerDelegate` formal protocol defines methods further configuring + the behaviour of `BITHockeyManager`, as well as the delegate of the modules it manages. + */ + +@protocol BITHockeyManagerDelegate + +@optional + + +///----------------------------------------------------------------------------- +/// @name App Identifier usage +///----------------------------------------------------------------------------- + +/** + Implement to force the usage of the live identifier + + This is useful if you are e.g. distributing an enterprise app inside your company + and want to use the `liveIdentifier` for that even though it is not running from + the App Store. + + Example: + + - (BOOL)shouldUseLiveIdentifierForHockeyManager:(BITHockeyManager *)hockeyManager { + #ifdef (CONFIGURATION_AppStore) + return YES; + #endif + return NO; + } + + @param hockeyManager BITHockeyManager instance + */ +- (BOOL)shouldUseLiveIdentifierForHockeyManager:(BITHockeyManager *)hockeyManager; + + +///----------------------------------------------------------------------------- +/// @name UI presentation +///----------------------------------------------------------------------------- + + +// optional parent view controller for the feedback screen when invoked via the alert view, default is the root UIWindow instance +/** + Return a custom parent view controller for presenting modal sheets + + By default the SDK is using the root UIWindow instance to present any required + view controllers. Overwrite this if this doesn't result in a satisfying + behavior or if you want to define any other parent view controller. + + @param hockeyManager The `BITHockeyManager` HockeyManager instance invoking this delegate + @param componentManager The `BITHockeyBaseManager` component instance invoking this delegate, can be `BITCrashManager` or `BITFeedbackManager` + */ +- (UIViewController *)viewControllerForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager; + + +///----------------------------------------------------------------------------- +/// @name Additional meta data +///----------------------------------------------------------------------------- + + +/** Return the userid that should used in the SDK components + + Right now this is used by the `BITCrashManager` to attach to a crash report. + `BITFeedbackManager` uses it too for assigning the user to a discussion thread. + + In addition, if this returns not nil for `BITFeedbackManager` the user will + not be asked for any user details by the component, including userName or userEmail. + + You can find out the component requesting the userID like this: + + - (NSString *)userIDForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager { + if (componentManager == hockeyManager.feedbackManager) { + return UserIDForFeedback; + } else if (componentManager == hockeyManager.crashManager) { + return UserIDForCrashReports; + } else { + return nil; + } + } + + For crash reports, this delegate is invoked on the startup after the crash! + + Alternatively you can also use `[BITHockeyManager userID]` which will cache the value in the keychain. + + @warning When returning a non nil value for the `BITCrashManager` component, crash reports + are not anonymous any more and the crash alerts will not show the word "anonymous"! + + @param hockeyManager The `BITHockeyManager` HockeyManager instance invoking this delegate + @param componentManager The `BITHockeyBaseManager` component instance invoking this delegate, can be `BITCrashManager` or `BITFeedbackManager` + @see userNameForHockeyManager:componentManager: + @see userEmailForHockeyManager:componentManager: + @see [BITHockeyManager userID] + */ +- (NSString *)userIDForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager; + + +/** Return the user name that should used in the SDK components + + Right now this is used by the `BITCrashManager` to attach to a crash report. + `BITFeedbackManager` uses it too for assigning the user to a discussion thread. + + In addition, if this returns not nil for `BITFeedbackManager` the user will + not be asked for any user details by the component, including userName or userEmail. + + You can find out the component requesting the user name like this: + + - (NSString *)userNameForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager { + if (componentManager == hockeyManager.feedbackManager) { + return UserNameForFeedback; + } else if (componentManager == hockeyManager.crashManager) { + return UserNameForCrashReports; + } else { + return nil; + } + } + + For crash reports, this delegate is invoked on the startup after the crash! + + Alternatively you can also use `[BITHockeyManager userName]` which will cache the value in the keychain. + + @warning When returning a non nil value for the `BITCrashManager` component, crash reports + are not anonymous any more and the crash alerts will not show the word "anonymous"! + + @param hockeyManager The `BITHockeyManager` HockeyManager instance invoking this delegate + @param componentManager The `BITHockeyBaseManager` component instance invoking this delegate, can be `BITCrashManager` or `BITFeedbackManager` + @see userIDForHockeyManager:componentManager: + @see userEmailForHockeyManager:componentManager: + @see [BITHockeyManager userName] + */ +- (NSString *)userNameForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager; + + +/** Return the users email address that should used in the SDK components + + Right now this is used by the `BITCrashManager` to attach to a crash report. + `BITFeedbackManager` uses it too for assigning the user to a discussion thread. + + In addition, if this returns not nil for `BITFeedbackManager` the user will + not be asked for any user details by the component, including userName or userEmail. + + You can find out the component requesting the user email like this: + + - (NSString *)userEmailForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager { + if (componentManager == hockeyManager.feedbackManager) { + return UserEmailForFeedback; + } else if (componentManager == hockeyManager.crashManager) { + return UserEmailForCrashReports; + } else { + return nil; + } + } + + For crash reports, this delegate is invoked on the startup after the crash! + + Alternatively you can also use `[BITHockeyManager userEmail]` which will cache the value in the keychain. + + @warning When returning a non nil value for the `BITCrashManager` component, crash reports + are not anonymous any more and the crash alerts will not show the word "anonymous"! + + @param hockeyManager The `BITHockeyManager` HockeyManager instance invoking this delegate + @param componentManager The `BITHockeyBaseManager` component instance invoking this delegate, can be `BITCrashManager` or `BITFeedbackManager` + @see userIDForHockeyManager:componentManager: + @see userNameForHockeyManager:componentManager: + @see [BITHockeyManager userEmail] + */ +- (NSString *)userEmailForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITImageAnnotation.h b/submodules/HockeySDK-iOS/Classes/BITImageAnnotation.h new file mode 100644 index 0000000000..c6788e41f8 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITImageAnnotation.h @@ -0,0 +1,40 @@ +/* + * Author: Moritz Haarmann + * + * 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 + +@interface BITImageAnnotation : UIView + +@property (nonatomic, getter=isSelected) BOOL selected; +@property (nonatomic) CGSize movedDelta; +@property (nonatomic, weak) UIImage *sourceImage; +@property (nonatomic) CGRect imageFrame; + +- (BOOL)resizable; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITImageAnnotation.m b/submodules/HockeySDK-iOS/Classes/BITImageAnnotation.m new file mode 100644 index 0000000000..d7bc699471 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITImageAnnotation.m @@ -0,0 +1,43 @@ +/* + * Author: Moritz Haarmann + * + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "BITImageAnnotation.h" + +@implementation BITImageAnnotation + +-(BOOL)resizable { + return NO; +} + +@end + +#endif diff --git a/submodules/HockeySDK-iOS/Classes/BITImageAnnotationViewController.h b/submodules/HockeySDK-iOS/Classes/BITImageAnnotationViewController.h new file mode 100644 index 0000000000..026968ceb3 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITImageAnnotationViewController.h @@ -0,0 +1,45 @@ +/* + * Author: Moritz Haarmann + * + * 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 + +@class BITImageAnnotationViewController; + +@protocol BITImageAnnotationDelegate + +- (void)annotationControllerDidCancel:(BITImageAnnotationViewController *)annotationController; +- (void)annotationController:(BITImageAnnotationViewController *)annotationController didFinishWithImage:(UIImage *)image; + +@end + +@interface BITImageAnnotationViewController : UIViewController + +@property (nonatomic, strong) UIImage *image; +@property (nonatomic, weak) id delegate; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITImageAnnotationViewController.m b/submodules/HockeySDK-iOS/Classes/BITImageAnnotationViewController.m new file mode 100644 index 0000000000..f8d95972b3 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITImageAnnotationViewController.m @@ -0,0 +1,415 @@ +/* + * Author: Moritz Haarmann + * + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "BITImageAnnotationViewController.h" +#import "BITImageAnnotation.h" +#import "BITRectangleImageAnnotation.h" +#import "BITArrowImageAnnotation.h" +#import "BITBlurImageAnnotation.h" +#import "BITHockeyHelper.h" +#import "HockeySDKPrivate.h" + +typedef NS_ENUM(NSInteger, BITImageAnnotationViewControllerInteractionMode) { + BITImageAnnotationViewControllerInteractionModeNone, + BITImageAnnotationViewControllerInteractionModeDraw, + BITImageAnnotationViewControllerInteractionModeMove +}; + +@interface BITImageAnnotationViewController () + +@property (nonatomic, strong) UIImageView *imageView; +@property (nonatomic, strong) UISegmentedControl *editingControls; +@property (nonatomic, strong) NSMutableArray *objects; + +@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; +@property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer; +@property (nonatomic, strong) UIPinchGestureRecognizer *pinchRecognizer; + +@property (nonatomic) CGFloat scaleFactor; + +@property (nonatomic) CGPoint panStart; +@property (nonatomic,strong) BITImageAnnotation *currentAnnotation; + +@property (nonatomic) BITImageAnnotationViewControllerInteractionMode currentInteraction; + +@property (nonatomic) CGRect pinchStartingFrame; + +@end + +@implementation BITImageAnnotationViewController + +#pragma mark - UIViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; + + NSArray *icons = @[@"Arrow.png",@"Rectangle.png", @"Blur.png"]; + + self.editingControls = [[UISegmentedControl alloc] initWithItems:@[@"Rectangle", @"Arrow", @"Blur"]]; + int i=0; + for (NSString *imageName in icons){ + [self.editingControls setImage:bit_imageNamed(imageName, BITHOCKEYSDK_BUNDLE) forSegmentAtIndex:i++]; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self.editingControls setSegmentedControlStyle:UISegmentedControlStyleBar]; +#pragma clang diagnostic pop + + self.navigationItem.titleView = self.editingControls; + + self.objects = [NSMutableArray new]; + + [self.editingControls addTarget:self action:@selector(editingAction:) forControlEvents:UIControlEventTouchUpInside]; + [self.editingControls setSelectedSegmentIndex:0]; + + self.imageView = [[UIImageView alloc] initWithFrame:self.view.bounds]; + + self.imageView.clipsToBounds = YES; + + self.imageView.image = self.image; + self.imageView.contentMode = UIViewContentModeScaleToFill; + + self.view.frame = UIScreen.mainScreen.bounds; + + [self.view addSubview:self.imageView]; + // Erm. + self.imageView.frame = [UIScreen mainScreen].bounds; + + self.panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panned:)]; + self.pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinched:)]; + self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; + + [self.imageView addGestureRecognizer:self.pinchRecognizer]; + [self.imageView addGestureRecognizer:self.panRecognizer]; + [self.view addGestureRecognizer:self.tapRecognizer]; + + self.imageView.userInteractionEnabled = YES; + + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc ] initWithImage:bit_imageNamed(@"Cancel.png", BITHOCKEYSDK_BUNDLE) landscapeImagePhone:bit_imageNamed(@"Cancel.png", BITHOCKEYSDK_BUNDLE) style:UIBarButtonItemStylePlain target:self action:@selector(discard:)]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc ] initWithImage:bit_imageNamed(@"Ok.png", BITHOCKEYSDK_BUNDLE) landscapeImagePhone:bit_imageNamed(@"Ok.png", BITHOCKEYSDK_BUNDLE) style:UIBarButtonItemStylePlain target:self action:@selector(save:)]; + + self.view.autoresizesSubviews = NO; +} + + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; + + [self fitImageViewFrame]; + +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; +} + +- (BOOL)prefersStatusBarHidden { + return self.navigationController.navigationBarHidden || self.navigationController.navigationBar.alpha == 0; +} + +- (void)orientationDidChange:(NSNotification *) __unused notification { + [self fitImageViewFrame]; +} + + +- (void)fitImageViewFrame { + + CGSize size = [UIScreen mainScreen].bounds.size; + if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation) && size.height > size.width){ + size = CGSizeMake(size.height, size.width); + } + + CGFloat heightScaleFactor = size.height / self.image.size.height; + CGFloat widthScaleFactor = size.width / self.image.size.width; + + CGFloat factor = MIN(heightScaleFactor, widthScaleFactor); + self.scaleFactor = factor; + CGSize scaledImageSize = CGSizeMake(self.image.size.width * factor, self.image.size.height * factor); + + CGRect baseFrame = CGRectMake(self.view.frame.size.width/2 - scaledImageSize.width/2, self.view.frame.size.height/2 - scaledImageSize.height/2, scaledImageSize.width, scaledImageSize.height); + + self.imageView.frame = baseFrame; +} + +- (void)editingAction:(id) __unused sender { + +} + +- (BITImageAnnotation *)annotationForCurrentMode { + if (self.editingControls.selectedSegmentIndex == 0){ + return [[BITArrowImageAnnotation alloc] initWithFrame:CGRectZero]; + } else if(self.editingControls.selectedSegmentIndex==1){ + return [[BITRectangleImageAnnotation alloc] initWithFrame:CGRectZero]; + } else { + return [[BITBlurImageAnnotation alloc] initWithFrame:CGRectZero]; + } +} + +#pragma mark - Actions + +- (void)discard:(id) __unused sender { + [self.delegate annotationControllerDidCancel:self]; + [self dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)save:(id) __unused sender { + UIImage *image = [self extractImage]; + [self.delegate annotationController:self didFinishWithImage:image]; + [self dismissViewControllerAnimated:YES completion:nil]; +} + +- (UIImage *)extractImage { + UIGraphicsBeginImageContextWithOptions(self.image.size, YES, 0.0); + CGContextRef ctx = UIGraphicsGetCurrentContext(); + [self.image drawInRect:CGRectMake(0, 0, self.image.size.width, self.image.size.height)]; + CGContextScaleCTM(ctx,((CGFloat)1.0)/self.scaleFactor,((CGFloat)1.0)/self.scaleFactor); + + // Drawing all the annotations onto the final image. + for (BITImageAnnotation *annotation in self.objects){ + CGContextTranslateCTM(ctx, annotation.frame.origin.x, annotation.frame.origin.y); + [annotation.layer renderInContext:ctx]; + CGContextTranslateCTM(ctx,-1 * annotation.frame.origin.x,-1 * annotation.frame.origin.y); + } + + UIImage *renderedImageOfMyself = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return renderedImageOfMyself; +} + +#pragma mark - UIGestureRecognizers + +- (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { + BITImageAnnotation *annotationAtLocation = (BITImageAnnotation *)[self.view hitTest:[gestureRecognizer locationInView:self.view] withEvent:nil]; + + if (![annotationAtLocation isKindOfClass:[BITImageAnnotation class]]){ + annotationAtLocation = nil; + } + + // determine the interaction mode if none is set so far. + + if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeNone){ + if (annotationAtLocation){ + self.currentInteraction = BITImageAnnotationViewControllerInteractionModeMove; + } else if ([self canDrawNewAnnotation]){ + self.currentInteraction = BITImageAnnotationViewControllerInteractionModeDraw; + } + } + + if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeNone){ + return; + } + + + if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeDraw){ + if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ + self.currentAnnotation = [self annotationForCurrentMode]; + [self.objects addObject:self.currentAnnotation]; + self.currentAnnotation.sourceImage = self.image; + + if (self.imageView.subviews.count > 0 && [self.currentAnnotation isKindOfClass:[BITBlurImageAnnotation class]]){ + [self.imageView insertSubview:self.currentAnnotation belowSubview:[self firstAnnotationThatIsNotBlur]]; + } else { + [self.imageView addSubview:self.currentAnnotation]; + } + + self.panStart = [gestureRecognizer locationInView:self.imageView]; + + } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged){ + CGPoint bla = [gestureRecognizer locationInView:self.imageView]; + self.currentAnnotation.frame = CGRectMake(self.panStart.x, self.panStart.y, bla.x - self.panStart.x, bla.y - self.panStart.y); + self.currentAnnotation.movedDelta = CGSizeMake(bla.x - self.panStart.x, bla.y - self.panStart.y); + self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; + [self.currentAnnotation setNeedsLayout]; + [self.currentAnnotation layoutIfNeeded]; + } else { + [self.currentAnnotation setSelected:NO]; + self.currentAnnotation = nil; + self.currentInteraction = BITImageAnnotationViewControllerInteractionModeNone; + } + } else if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeMove){ + if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ + // find and possibly move an existing annotation. + + + if ([self.objects indexOfObject:annotationAtLocation] != NSNotFound){ + self.currentAnnotation = annotationAtLocation; + [annotationAtLocation setSelected:YES]; + } + + + } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged && self.currentAnnotation){ + CGPoint delta = [gestureRecognizer translationInView:self.view]; + + CGRect annotationFrame = self.currentAnnotation.frame; + annotationFrame.origin.x += delta.x; + annotationFrame.origin.y += delta.y; + self.currentAnnotation.frame = annotationFrame; + self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; + + [self.currentAnnotation setNeedsLayout]; + [self.currentAnnotation layoutIfNeeded]; + + [gestureRecognizer setTranslation:CGPointZero inView:self.view]; + + } else { + [self.currentAnnotation setSelected:NO]; + self.currentAnnotation = nil; + self.currentInteraction = BITImageAnnotationViewControllerInteractionModeNone; + } + } +} + +- (void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ + // try to figure out which view we are talking about. + BITImageAnnotation *candidate = nil; + BOOL validView = YES; + + for (uint i = 0; i < gestureRecognizer.numberOfTouches; i++){ + BITImageAnnotation *newCandidate = (BITImageAnnotation *)[self.view hitTest:[gestureRecognizer locationOfTouch:i inView:self.view] withEvent:nil]; + + if (![newCandidate isKindOfClass:[BITImageAnnotation class]]){ + newCandidate = nil; + } + + if (candidate == nil){ + candidate = newCandidate; + } else if (candidate != newCandidate){ + validView = NO; + break; + } + } + + if (validView && [candidate resizable]){ + self.currentAnnotation = candidate; + self.pinchStartingFrame = self.currentAnnotation.frame; + [self.currentAnnotation setSelected:YES]; + } + + } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged && self.currentAnnotation && gestureRecognizer.numberOfTouches>1){ + CGRect newFrame= (self.pinchStartingFrame); + + // upper point? + CGPoint point1 = [gestureRecognizer locationOfTouch:0 inView:self.view]; + CGPoint point2 = [gestureRecognizer locationOfTouch:1 inView:self.view]; + + + newFrame.origin.x = point1.x; + newFrame.origin.y = point1.y; + + newFrame.origin.x = (point1.x > point2.x) ? point2.x : point1.x; + newFrame.origin.y = (point1.y > point2.y) ? point2.y : point1.y; + + newFrame.size.width = (point1.x > point2.x) ? point1.x - point2.x : point2.x - point1.x; + newFrame.size.height = (point1.y > point2.y) ? point1.y - point2.y : point2.y - point1.y; + + + self.currentAnnotation.frame = newFrame; + self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; + } else { + [self.currentAnnotation setSelected:NO]; + self.currentAnnotation = nil; + } +} + +- (void)tapped:(UIGestureRecognizer *) __unused tapRecognizer { + + // TODO: remove pre-iOS 8 code. + + // This toggles the nav and status bar. Since iOS7 and pre-iOS7 behave weirdly different, + // this might look rather hacky, but hiding the navbar under iOS6 leads to some ugly + // animation effect which is avoided by simply hiding the navbar setting it's alpha to 0. // moritzh + + if (self.navigationController.navigationBar.alpha == 0 || self.navigationController.navigationBarHidden ){ + + [UIView animateWithDuration:0.35 animations:^{ + [self.navigationController setNavigationBarHidden:NO animated:NO]; + + if ([self respondsToSelector:@selector(prefersStatusBarHidden)]) { + [self setNeedsStatusBarAppearanceUpdate]; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [[UIApplication sharedApplication] setStatusBarHidden:NO]; +#pragma clang diagnostic pop + } + + } completion:^(BOOL __unused finished) { + [self fitImageViewFrame]; + + }]; + } else { + [UIView animateWithDuration:0.35 animations:^{ + [self.navigationController setNavigationBarHidden:YES animated:NO]; + + if ([self respondsToSelector:@selector(prefersStatusBarHidden)]) { + [self setNeedsStatusBarAppearanceUpdate]; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [[UIApplication sharedApplication] setStatusBarHidden:YES]; +#pragma clang diagnostic pop + } + + } completion:^(BOOL __unused finished) { + [self fitImageViewFrame]; + + }]; + } + +} + +#pragma mark - Helpers + +- (UIView *)firstAnnotationThatIsNotBlur { + for (BITImageAnnotation *annotation in self.imageView.subviews){ + if (![annotation isKindOfClass:[BITBlurImageAnnotation class]]){ + return annotation; + } + } + + return self.imageView; +} + +- (BOOL)canDrawNewAnnotation { + return [self.editingControls selectedSegmentIndex] != UISegmentedControlNoSegment; +} +@end + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITInternal.h b/submodules/HockeySDK-iOS/Classes/BITInternal.h new file mode 100644 index 0000000000..9cbe2efde1 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITInternal.h @@ -0,0 +1,8 @@ +#import "BITTelemetryObject.h" + +@interface BITInternal : BITTelemetryObject + +@property (nonatomic, copy) NSString *sdkVersion; +@property (nonatomic, copy) NSString *agentVersion; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITInternal.m b/submodules/HockeySDK-iOS/Classes/BITInternal.m new file mode 100644 index 0000000000..ed4d46e4a2 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITInternal.m @@ -0,0 +1,39 @@ +#import "BITInternal.h" + +/// Data contract class for type Internal. +@implementation BITInternal + +/// +/// Adds all members of this class to a dictionary +/// @returns dictionary to which the members of this class will be added. +/// +- (NSDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary].mutableCopy; + if (self.sdkVersion != nil) { + [dict setObject:self.sdkVersion forKey:@"ai.internal.sdkVersion"]; + } + if (self.agentVersion != nil) { + [dict setObject:self.agentVersion forKey:@"ai.internal.agentVersion"]; + } + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if(self) { + _sdkVersion = [coder decodeObjectForKey:@"self.sdkVersion"]; + _agentVersion = [coder decodeObjectForKey:@"self.agentVersion"]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.sdkVersion forKey:@"self.sdkVersion"]; + [coder encodeObject:self.agentVersion forKey:@"self.agentVersion"]; +} + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITKeychainUtils.h b/submodules/HockeySDK-iOS/Classes/BITKeychainUtils.h new file mode 100644 index 0000000000..b7d7981166 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITKeychainUtils.h @@ -0,0 +1,44 @@ +// +// SFHFKeychainUtils.h +// +// Created by Buzz Andersen on 10/20/08. +// Based partly on code by Jonathan Wight, Jon Crosby, and Mike Malone. +// Copyright 2008 Sci-Fi Hi-Fi. 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 + + +@interface BITKeychainUtils : NSObject { + +} + ++ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error; +//uses the default kSecAttrAccessibleWhenUnlocked ++ (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error; ++ (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting accessibility:(CFTypeRef) accessibility error: (NSError **) error; ++ (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error; ++ (BOOL) purgeItemsForServiceName:(NSString *) serviceName error: (NSError **) error; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITKeychainUtils.m b/submodules/HockeySDK-iOS/Classes/BITKeychainUtils.m new file mode 100644 index 0000000000..9ec9eddb98 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITKeychainUtils.m @@ -0,0 +1,313 @@ +// +// SFHFKeychainUtils.m +// +// Created by Buzz Andersen on 10/20/08. +// Based partly on code by Jonathan Wight, Jon Crosby, and Mike Malone. +// Copyright 2008 Sci-Fi Hi-Fi. 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 "BITKeychainUtils.h" +#import + +static NSString *BITKeychainUtilsErrorDomain = @"BITKeychainUtilsErrorDomain"; + +@implementation BITKeychainUtils + ++ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError *__autoreleasing *) error { + if (!username || !serviceName) { + if (error != nil) { + *error = [NSError errorWithDomain: BITKeychainUtilsErrorDomain code: -2000 userInfo: nil]; + } + return nil; + } + + if (error != nil) { + *error = nil; + } + + // Set up a query dictionary with the base query attributes: item type (generic), username, and service + + NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, kSecAttrAccount, kSecAttrService, nil]; + NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, username, serviceName, nil]; + + NSMutableDictionary *query = [[NSMutableDictionary alloc] initWithObjects: objects forKeys: keys]; + + // First do a query for attributes, in case we already have a Keychain item with no password data set. + // One likely way such an incorrect item could have come about is due to the previous (incorrect) + // version of this code (which set the password as a generic attribute instead of password data). + + NSMutableDictionary *attributeQuery = [query mutableCopy]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-qual" + [attributeQuery setObject: (id) kCFBooleanTrue forKey:(__bridge_transfer id) kSecReturnAttributes]; +#pragma clang diagnostic pop + CFTypeRef attrResult = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef) attributeQuery, &attrResult); +// NSDictionary *attributeResult = (__bridge_transfer NSDictionary *)attrResult; + if (attrResult) + CFRelease(attrResult); + + if (status != noErr) { + // No existing item found--simply return nil for the password + if (error != nil && status != errSecItemNotFound) { + //Only return an error if a real exception happened--not simply for "not found." + *error = [NSError errorWithDomain: BITKeychainUtilsErrorDomain code: status userInfo: nil]; + } + + return nil; + } + + // We have an existing item, now query for the password data associated with it. + + NSMutableDictionary *passwordQuery = [query mutableCopy]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-qual" + [passwordQuery setObject: (id) kCFBooleanTrue forKey: (__bridge_transfer id) kSecReturnData]; +#pragma clang diagnostic pop + CFTypeRef resData = NULL; + status = SecItemCopyMatching((__bridge CFDictionaryRef) passwordQuery, (CFTypeRef *) &resData); + NSData *resultData = (__bridge_transfer NSData *)resData; + + if (status != noErr) { + if (status == errSecItemNotFound) { + // We found attributes for the item previously, but no password now, so return a special error. + // Users of this API will probably want to detect this error and prompt the user to + // re-enter their credentials. When you attempt to store the re-entered credentials + // using storeUsername:andPassword:forServiceName:updateExisting:error + // the old, incorrect entry will be deleted and a new one with a properly encrypted + // password will be added. + if (error != nil) { + *error = [NSError errorWithDomain: BITKeychainUtilsErrorDomain code: -1999 userInfo: nil]; + } + } + else { + // Something else went wrong. Simply return the normal Keychain API error code. + if (error != nil) { + *error = [NSError errorWithDomain: BITKeychainUtilsErrorDomain code: status userInfo: nil]; + } + } + + return nil; + } + + NSString *password = nil; + + if (resultData) { + password = [[NSString alloc] initWithData: resultData encoding: NSUTF8StringEncoding]; + } + else { + // There is an existing item, but we weren't able to get password data for it for some reason, + // Possibly as a result of an item being incorrectly entered by the previous code. + // Set the -1999 error so the code above us can prompt the user again. + if (error != nil) { + *error = [NSError errorWithDomain: BITKeychainUtilsErrorDomain code: -1999 userInfo: nil]; + } + } + + return password; +} + ++ (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError *__autoreleasing *) error { + return [self storeUsername:username andPassword:password forServiceName:serviceName updateExisting:updateExisting accessibility:kSecAttrAccessibleAlways error:error]; +} + ++ (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting accessibility:(CFTypeRef) accessibility error: (NSError *__autoreleasing *) error +{ + if (!username || !password || !serviceName) + { + if (error != nil) + { + *error = [NSError errorWithDomain: BITKeychainUtilsErrorDomain code: -2000 userInfo: nil]; + } + return NO; + } + + // See if we already have a password entered for these credentials. + NSError *getError = nil; + NSString *existingPassword = [BITKeychainUtils getPasswordForUsername: username andServiceName: serviceName error:&getError]; + + if ([getError code] == -1999) + { + // There is an existing entry without a password properly stored (possibly as a result of the previous incorrect version of this code. + // Delete the existing item before moving on entering a correct one. + + getError = nil; + + [self deleteItemForUsername: username andServiceName: serviceName error: &getError]; + + if ([getError code] != noErr) + { + if (error != nil) + { + *error = getError; + } + return NO; + } + } + else if ([getError code] != noErr) + { + if (error != nil) + { + *error = getError; + } + return NO; + } + + if (error != nil) + { + *error = nil; + } + + OSStatus status = noErr; + + if (existingPassword) + { + // We have an existing, properly entered item with a password. + // Update the existing item. + + if (![existingPassword isEqualToString:password] && updateExisting) + { + //Only update if we're allowed to update existing. If not, simply do nothing. + + NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, + kSecAttrService, + kSecAttrLabel, + kSecAttrAccount, + kSecAttrAccessible, + nil]; + + NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, + serviceName, + serviceName, + username, + accessibility, + nil]; + + NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys]; + + status = SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) [NSDictionary dictionaryWithObject: (NSData *)[password dataUsingEncoding: NSUTF8StringEncoding] forKey: (__bridge_transfer NSString *) kSecValueData]); + } + } + else + { + // No existing entry (or an existing, improperly entered, and therefore now + // deleted, entry). Create a new entry. + + NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, + kSecAttrService, + kSecAttrLabel, + kSecAttrAccount, + kSecValueData, + kSecAttrAccessible, + nil]; + + NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, + serviceName, + serviceName, + username, + [password dataUsingEncoding: NSUTF8StringEncoding], + accessibility, + nil]; + + NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys]; + + status = SecItemAdd((__bridge CFDictionaryRef) query, NULL); + } + + if (error != nil && status != noErr) + { + // Something went wrong with adding the new item. Return the Keychain error code. + *error = [NSError errorWithDomain: BITKeychainUtilsErrorDomain code: status userInfo: nil]; + + return NO; + } + + return YES; +} + ++ (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError *__autoreleasing *) error +{ + if (!username || !serviceName) + { + if (error != nil) + { + *error = [NSError errorWithDomain: BITKeychainUtilsErrorDomain code: -2000 userInfo: nil]; + } + return NO; + } + + if (error != nil) + { + *error = nil; + } + + NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, kSecAttrAccount, kSecAttrService, kSecReturnAttributes, nil]; + NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, username, serviceName, kCFBooleanTrue, nil]; + + NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys]; + + OSStatus status = SecItemDelete((__bridge CFDictionaryRef) query); + + if (error != nil && status != noErr) + { + *error = [NSError errorWithDomain: BITKeychainUtilsErrorDomain code: status userInfo: nil]; + + return NO; + } + + return YES; +} + ++ (BOOL) purgeItemsForServiceName:(NSString *) serviceName error: (NSError *__autoreleasing *) error { + if (!serviceName) + { + if (error != nil) + { + *error = [NSError errorWithDomain: BITKeychainUtilsErrorDomain code: -2000 userInfo: nil]; + } + return NO; + } + + if (error != nil) + { + *error = nil; + } + + NSMutableDictionary *searchData = [NSMutableDictionary new]; + [searchData setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; + [searchData setObject:serviceName forKey:(__bridge id)kSecAttrService]; + + OSStatus status = SecItemDelete((__bridge CFDictionaryRef)searchData); + + if (error != nil && status != noErr) + { + *error = [NSError errorWithDomain: BITKeychainUtilsErrorDomain code: status userInfo: nil]; + + return NO; + } + + return YES; +} + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITMetricsManager.h b/submodules/HockeySDK-iOS/Classes/BITMetricsManager.h new file mode 100644 index 0000000000..6d5e3f2d5f --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITMetricsManager.h @@ -0,0 +1,60 @@ +#import "HockeySDKFeatureConfig.h" + +#if HOCKEYSDK_FEATURE_METRICS + +#import +#import "BITHockeyBaseManager.h" + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +/** + The metrics module. + + This is the HockeySDK module that handles users, sessions and events tracking. + + Unless disabled, this module automatically tracks users and session of your app to give you + better insights about how your app is being used. + Users are tracked in a completely anonymous way without collecting any personally identifiable + information. + + Before starting to track events, ask yourself the questions that you want to get answers to. + For instance, you might be interested in business, performance/quality or user experience aspects. + Name your events in a meaningful way and keep in mind that you will use these names + when searching for events in the HockeyApp web portal. + + It is your reponsibility to not collect personal information as part of the events tracking or get + prior consent from your users as necessary. + */ +@interface BITMetricsManager : BITHockeyBaseManager + +/** + * A property indicating whether the BITMetricsManager instance is disabled. + */ +@property (nonatomic, assign) BOOL disabled; + +/** + * This method allows to track an event that happened in your app. + * Remember to choose meaningful event names to have the best experience when diagnosing your app + * in the HockeyApp web portal. + * + * @param eventName The event's name as a string. + */ +- (void)trackEventWithName:(nonnull NSString *)eventName; + +/** + * This method allows to track an event that happened in your app. + * Remember to choose meaningful event names to have the best experience when diagnosing your app + * in the web portal. + * + * @param eventName the name of the event, which should be tracked. + * @param properties key value pairs with additional info about the event. + * @param measurements key value pairs, which contain custom metrics. + */ +- (void)trackEventWithName:(nonnull NSString *)eventName properties:(nullable NSDictionary *)properties measurements:(nullable NSDictionary *)measurements; + +@end + +NS_ASSUME_NONNULL_END + +#endif /* HOCKEYSDK_FEATURE_METRICS */ diff --git a/submodules/HockeySDK-iOS/Classes/BITMetricsManager.m b/submodules/HockeySDK-iOS/Classes/BITMetricsManager.m new file mode 100644 index 0000000000..d7dbeeecc5 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITMetricsManager.m @@ -0,0 +1,291 @@ +#import "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_METRICS + +#import "BITMetricsManager.h" +#import "BITTelemetryContext.h" +#import "BITMetricsManagerPrivate.h" +#import "BITHockeyHelper.h" +#import "BITHockeyHelper+Application.h" +#import "HockeySDKPrivate.h" +#import "BITChannelPrivate.h" +#import "BITEventData.h" +#import "BITSession.h" +#import "BITSessionState.h" +#import "BITSessionStateData.h" +#import "BITPersistencePrivate.h" +#import "BITHockeyBaseManagerPrivate.h" +#import "BITSender.h" + +NSString *const kBITApplicationWasLaunched = @"BITApplicationWasLaunched"; + +static char *const kBITMetricsEventQueue = "net.hockeyapp.telemetryEventQueue"; + +static NSString *const kBITSessionFileType = @"plist"; +static NSString *const kBITApplicationDidEnterBackgroundTime = @"BITApplicationDidEnterBackgroundTime"; + +static NSString *const BITMetricsBaseURLString = @"https://gate.hockeyapp.net/"; +static NSString *const BITMetricsURLPathString = @"v2/track"; + +@interface BITMetricsManager () + +@property (nonatomic, strong) id appWillEnterForegroundObserver; +@property (nonatomic, strong) id appDidEnterBackgroundObserver; + +@end + +@implementation BITMetricsManager + +@synthesize channel = _channel; +@synthesize telemetryContext = _telemetryContext; +@synthesize persistence = _persistence; +@synthesize serverURL = _serverURL; +@synthesize userDefaults = _userDefaults; + +#pragma mark - Create & start instance + +- (instancetype)init { + if ((self = [super init])) { + _disabled = NO; + _metricsEventQueue = dispatch_queue_create(kBITMetricsEventQueue, DISPATCH_QUEUE_CONCURRENT); + _appBackgroundTimeBeforeSessionExpires = 20; + _serverURL = [NSString stringWithFormat:@"%@%@", BITMetricsBaseURLString, BITMetricsURLPathString]; + } + return self; +} + +- (instancetype)initWithChannel:(BITChannel *)channel telemetryContext:(BITTelemetryContext *)telemetryContext persistence:(BITPersistence *)persistence userDefaults:(NSUserDefaults *)userDefaults { + if ((self = [self init])) { + _channel = channel; + _telemetryContext = telemetryContext; + _persistence = persistence; + _userDefaults = userDefaults; + } + return self; +} + +- (void)startManager { + self.sender = [[BITSender alloc] initWithPersistence:self.persistence serverURL:(NSURL *)[NSURL URLWithString:self.serverURL]]; + [self.sender sendSavedDataAsync]; + [self startNewSessionWithId:bit_UUID()]; + [self registerObservers]; +} + +#pragma mark - Configuration + +- (void)setDisabled:(BOOL)disabled { + if (_disabled == disabled) { return; } + _disabled = disabled; + if (disabled) { + [self unregisterObservers]; + } else { + [self startManager]; + } +} + +#pragma mark - Sessions + +- (void)registerObservers { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + __weak typeof(self) weakSelf = self; + + if (nil == self.appDidEnterBackgroundObserver) { + self.appDidEnterBackgroundObserver = + [center addObserverForName:UIApplicationDidEnterBackgroundNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf updateDidEnterBackgroundTime]; + }]; + } + if (nil == self.appWillEnterForegroundObserver) { + self.appWillEnterForegroundObserver = + [center addObserverForName:UIApplicationWillEnterForegroundNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf startNewSessionIfNeeded]; + }]; + } +} + +- (void)unregisterObservers { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + id appDidEnterBackgroundObserver = self.appDidEnterBackgroundObserver; + if(appDidEnterBackgroundObserver) { + [center removeObserver:appDidEnterBackgroundObserver]; + self.appDidEnterBackgroundObserver = nil; + } + id appWillEnterForegroundObserver = self.appWillEnterForegroundObserver; + if(appWillEnterForegroundObserver) { + [center removeObserver:appWillEnterForegroundObserver]; + self.appWillEnterForegroundObserver = nil; + } +} + +- (void)updateDidEnterBackgroundTime { + [self.userDefaults setDouble:[[NSDate date] timeIntervalSince1970] forKey:kBITApplicationDidEnterBackgroundTime]; +} + +- (void)startNewSessionIfNeeded { + double appDidEnterBackgroundTime = [self.userDefaults doubleForKey:kBITApplicationDidEnterBackgroundTime]; + // Add safeguard in case this returns a negative value + if(appDidEnterBackgroundTime < 0) { + appDidEnterBackgroundTime = 0; + [self.userDefaults setDouble:0 forKey:kBITApplicationDidEnterBackgroundTime]; + } + double timeSinceLastBackground = [[NSDate date] timeIntervalSince1970] - appDidEnterBackgroundTime; + if (timeSinceLastBackground > self.appBackgroundTimeBeforeSessionExpires) { + [self startNewSessionWithId:bit_UUID()]; + } +} + +- (void)startNewSessionWithId:(NSString *)sessionId { + BITSession *newSession = [self createNewSessionWithId:sessionId]; + [self.telemetryContext setSessionId:newSession.sessionId]; + [self.telemetryContext setIsFirstSession:newSession.isFirst]; + [self.telemetryContext setIsNewSession:newSession.isNew]; + [self trackSessionWithState:BITSessionState_start]; +} + +- (BITSession *)createNewSessionWithId:(NSString *)sessionId { + BITSession *session = [BITSession new]; + session.sessionId = sessionId; + session.isNew = @"true"; + + if (![self.userDefaults boolForKey:kBITApplicationWasLaunched]) { + session.isFirst = @"true"; + [self.userDefaults setBool:YES forKey:kBITApplicationWasLaunched]; + } else { + session.isFirst = @"false"; + } + return session; +} + +#pragma mark - Track telemetry + +#pragma mark Sessions + +- (void)trackSessionWithState:(BITSessionState)state { + if (self.disabled) { + BITHockeyLogDebug(@"INFO: BITMetricsManager is disabled, therefore this tracking call was ignored."); + return; + } + BITSessionStateData *sessionStateData = [BITSessionStateData new]; + sessionStateData.state = state; + [self.channel enqueueTelemetryItem:sessionStateData]; +} + +#pragma mark Events + +- (void)trackEventWithName:(nonnull NSString *)eventName { + if (!eventName) { + return; + } + if (self.disabled) { + BITHockeyLogDebug(@"INFO: BITMetricsManager is disabled, therefore this tracking call was ignored."); + return; + } + + __weak typeof(self) weakSelf = self; + dispatch_group_t group = dispatch_group_create(); + dispatch_group_async(group, self.metricsEventQueue, ^{ + typeof(self) strongSelf = weakSelf; + BITEventData *eventData = [BITEventData new]; + [eventData setName:eventName]; + [strongSelf trackDataItem:eventData]; + }); + + // If the app is running in the background. + UIApplication *application = [UIApplication sharedApplication]; + BOOL applicationIsInBackground = ([BITHockeyHelper applicationState] == BITApplicationStateBackground); + if (applicationIsInBackground) { + [self.channel createBackgroundTaskWhileDataIsSending:application withWaitingGroup:group]; + } +} + +- (void)trackEventWithName:(nonnull NSString *)eventName + properties:(nullable NSDictionary *)properties + measurements:(nullable NSDictionary *)measurements { + if (!eventName) { + return; + } + if (self.disabled) { + BITHockeyLogDebug(@"INFO: BITMetricsManager is disabled, therefore this tracking call was ignored."); + return; + } + + __weak typeof(self) weakSelf = self; + dispatch_group_t group = dispatch_group_create(); + dispatch_group_async(group, self.metricsEventQueue, ^{ + typeof(self) strongSelf = weakSelf; + BITEventData *eventData = [BITEventData new]; + [eventData setName:eventName]; + [eventData setProperties:(NSDictionary *)properties]; + [eventData setMeasurements:measurements]; + [strongSelf trackDataItem:eventData]; + }); + + // If the app is running in the background. + UIApplication *application = [UIApplication sharedApplication]; + BOOL applicationIsInBackground = ([BITHockeyHelper applicationState] == BITApplicationStateBackground); + if (applicationIsInBackground) { + [self.channel createBackgroundTaskWhileDataIsSending:application withWaitingGroup:group]; + } +} + +#pragma mark Track DataItem + +- (void)trackDataItem:(BITTelemetryData *)dataItem { + if (self.disabled) { + BITHockeyLogDebug(@"INFO: BITMetricsManager is disabled, therefore this tracking call was ignored."); + return; + } + + BITHockeyLogDebug(@"INFO: Enqueue telemetry item: %@", dataItem.name); + [self.channel enqueueTelemetryItem:dataItem]; +} + +#pragma mark - Custom getter + +- (BITChannel *)channel { + @synchronized(self) { + if (!_channel) { + _channel = [[BITChannel alloc] initWithTelemetryContext:self.telemetryContext persistence:self.persistence]; + } + return _channel; + } +} + +- (BITTelemetryContext *)telemetryContext { + @synchronized(self) { + if (!_telemetryContext) { + _telemetryContext = [[BITTelemetryContext alloc] initWithAppIdentifier:self.appIdentifier persistence:self.persistence]; + } + return _telemetryContext; + } +} + +- (BITPersistence *)persistence { + @synchronized(self) { + if (!_persistence) { + _persistence = [BITPersistence new]; + } + return _persistence; + } +} + +- (NSUserDefaults *)userDefaults { + @synchronized(self) { + if (!_userDefaults) { + _userDefaults = [NSUserDefaults standardUserDefaults]; + } + return _userDefaults; + } +} + +@end + +#endif /* HOCKEYSDK_FEATURE_METRICS */ diff --git a/submodules/HockeySDK-iOS/Classes/BITMetricsManagerPrivate.h b/submodules/HockeySDK-iOS/Classes/BITMetricsManagerPrivate.h new file mode 100644 index 0000000000..cee0f9ae76 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITMetricsManagerPrivate.h @@ -0,0 +1,125 @@ +#import "HockeySDKFeatureConfig.h" + +#if HOCKEYSDK_FEATURE_METRICS + +#import "BITMetricsManager.h" +#import "BITSessionState.h" + +@class BITChannel; +@class BITTelemetryContext; +@class BITSession; +@class BITPersistence; +@class BITSender; + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +FOUNDATION_EXPORT NSString *const kBITApplicationWasLaunched; + +@interface BITMetricsManager() + +/** + * Create a new BITMetricsManager instance by passing the channel, the telemetry context, and persistence instance to use + for processing metrics. This method can be used for dependency injection. + */ +- (instancetype)initWithChannel:(BITChannel *)channel + telemetryContext:(BITTelemetryContext *)telemetryContext + persistence:(BITPersistence *)persistence + userDefaults:(NSUserDefaults *)userDefaults; + +/** + * The user defaults object used to store meta data. + */ +@property (nonatomic, strong, readonly) NSUserDefaults *userDefaults; + +/** + * A channel for collecting new events before storing and sending them. + */ +@property (nonatomic, strong, readonly) BITPersistence *persistence; + +/** + * A channel for collecting new events before storing and sending them. + */ +@property (nonatomic, strong, readonly) BITChannel *channel; + +/** + * A telemetry context which is used to add meta info to events, before they're sent out. + */ +@property (nonatomic, strong, readonly) BITTelemetryContext *telemetryContext; + +/** + * A concurrent queue which creates and processes telemetry items. + */ +@property (nonatomic, strong, readonly) dispatch_queue_t metricsEventQueue; + +/** + * Sender instance to send out telemetry data. + */ +@property (nonatomic, strong) BITSender *sender; + +///----------------------------------------------------------------------------- +/// @name Session Management +///----------------------------------------------------------------------------- + +/** + * The Interval an app has to be in the background until the current session gets renewed. + */ +@property (nonatomic, assign) NSUInteger appBackgroundTimeBeforeSessionExpires; + +/** + * Registers manager for several notifications, which influence the session state. + */ +- (void)registerObservers; + +/** + * Unregisters manager for several notifications, which influence the session state. + */ +- (void)unregisterObservers; + +/** + * Stores the current date before app is sent to background. + * + * @see appBackgroundTimeBeforeSessionExpires + * @see startNewSessionIfNeeded + */ +- (void)updateDidEnterBackgroundTime; + +/** + * Determines whether the current session needs to be renewed or not. + * + * @see appBackgroundTimeBeforeSessionExpires + * @see updateDidEnterBackgroundTime + */ +- (void)startNewSessionIfNeeded; + +/** + * Creates a new session, updates the session context and sends it to the channel. + * + * @param sessionId the id for the new session + */ +- (void)startNewSessionWithId:(NSString *)sessionId; + +/** + * Creates a new session and stores it to NSUserDefaults. + * + * @param sessionId the id for the new session + * @return the newly created session + */ +- (BITSession *)createNewSessionWithId:(NSString *)sessionId; + +///----------------------------------------------------------------------------- +/// @name Track telemetry data +///----------------------------------------------------------------------------- + +/** + * Creates and enqueues a session event for the given state. + * + * @param state value that determines whether the session started or ended + */ +- (void)trackSessionWithState:(BITSessionState)state; + +@end + +NS_ASSUME_NONNULL_END + +#endif /* HOCKEYSDK_FEATURE_METRICS */ diff --git a/submodules/HockeySDK-iOS/Classes/BITPersistence.h b/submodules/HockeySDK-iOS/Classes/BITPersistence.h new file mode 100644 index 0000000000..dd2ac4f158 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITPersistence.h @@ -0,0 +1,9 @@ +#import + +/** + * A simple class that handles serialisation and deserialisation of bundles of data. + */ +@interface BITPersistence : NSObject + +@end + diff --git a/submodules/HockeySDK-iOS/Classes/BITPersistence.m b/submodules/HockeySDK-iOS/Classes/BITPersistence.m new file mode 100644 index 0000000000..cd3646af81 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITPersistence.m @@ -0,0 +1,305 @@ +#import "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_METRICS + +#import "BITPersistence.h" +#import "BITPersistencePrivate.h" +#import "HockeySDKPrivate.h" +#import "BITHockeyHelper.h" + +NSString *const BITPersistenceSuccessNotification = @"BITHockeyPersistenceSuccessNotification"; + +static NSString *const kBITTelemetry = @"Telemetry"; +static NSString *const kBITMetaData = @"MetaData"; +static NSString *const kBITFileBaseString = @"hockey-app-bundle-"; +static NSString *const kBITFileBaseStringMeta = @"metadata"; + +static NSString *const kBITHockeyDirectory = @"com.microsoft.HockeyApp"; +static NSString *const kBITTelemetryDirectory = @"Telemetry"; +static NSString *const kBITMetaDataDirectory = @"MetaData"; + +static char const *kBITPersistenceQueueString = "com.microsoft.HockeyApp.persistenceQueue"; +static NSUInteger const BITDefaultFileCount = 50; + +@interface BITPersistence () + +@property (nonatomic) BOOL directorySetupComplete; + +@end + +@implementation BITPersistence + +#pragma mark - Public + +- (instancetype)init { + self = [super init]; + if (self) { + _persistenceQueue = dispatch_queue_create(kBITPersistenceQueueString, DISPATCH_QUEUE_SERIAL); //TODO several queues? + _requestedBundlePaths = [NSMutableArray new]; + _maxFileCount = BITDefaultFileCount; + + // Evantually, there will be old files on disk, the flag will be updated before the first event gets created + _directorySetupComplete = NO; //will be set to true in createDirectoryStructureIfNeeded + + [self createDirectoryStructureIfNeeded]; + } + return self; +} + +/** + * Saves the Bundle using NSKeyedArchiver and NSData's writeToFile:atomically + * Sends out a BITHockeyPersistenceSuccessNotification in case of success + */ +- (void)persistBundle:(NSData *)bundle { + //TODO send out a fail notification? + NSString *fileURL = [self fileURLForType:BITPersistenceTypeTelemetry]; + + if (bundle) { + __weak typeof(self) weakSelf = self; + dispatch_async(self.persistenceQueue, ^{ + typeof(self) strongSelf = weakSelf; + BOOL success = [bundle writeToFile:fileURL atomically:YES]; + if (success) { + BITHockeyLogDebug(@"INFO: Wrote bundle to %@", fileURL); + [strongSelf sendBundleSavedNotification]; + } + else { + BITHockeyLogError(@"Error writing bundle to %@", fileURL); + } + }); + } + else { + BITHockeyLogWarning(@"WARNING: Unable to write %@ as provided bundle was null", fileURL); + } +} + +- (void)persistMetaData:(NSDictionary *)metaData { + NSString *fileURL = [self fileURLForType:BITPersistenceTypeMetaData]; + //TODO send out a notification, too?! + dispatch_async(self.persistenceQueue, ^{ + [NSKeyedArchiver archiveRootObject:metaData toFile:fileURL]; + }); +} + +- (BOOL)isFreeSpaceAvailable { + NSArray *files = [self persistedFilesForType:BITPersistenceTypeTelemetry]; + return files.count < self.maxFileCount; +} + +- (NSString *)requestNextFilePath { + __block NSString *path = nil; + __weak typeof(self) weakSelf = self; + dispatch_sync(self.persistenceQueue, ^() { + typeof(self) strongSelf = weakSelf; + + path = [strongSelf nextURLOfType:BITPersistenceTypeTelemetry]; + + if (path) { + [self.requestedBundlePaths addObject:path]; + } + }); + return path; +} + +- (NSDictionary *)metaData { + NSString *filePath = [self fileURLForType:BITPersistenceTypeMetaData]; + NSObject *bundle = [self bundleAtFilePath:filePath withFileBaseString:kBITFileBaseStringMeta]; + if ([bundle isKindOfClass:NSDictionary.class]) { + return (NSDictionary *) bundle; + } + BITHockeyLogDebug(@"INFO: The context meta data file could not be loaded."); + return [NSDictionary dictionary]; +} + +- (NSObject *)bundleAtFilePath:(NSString *)filePath withFileBaseString:(NSString *)filebaseString { + id bundle = nil; + if (filePath && [filePath rangeOfString:filebaseString].location != NSNotFound) { + bundle = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath]; + } + return bundle; +} + +- (NSData *)dataAtFilePath:(NSString *)path { + NSData *data = nil; + if (path && [path rangeOfString:kBITFileBaseString].location != NSNotFound) { + data = [NSData dataWithContentsOfFile:path]; + } + return data; +} + +/** + * Deletes a file at the given path. + * + * @param path The path to look for a file and delete it. + */ +- (void)deleteFileAtPath:(NSString *)path { + __weak typeof(self) weakSelf = self; + dispatch_sync(self.persistenceQueue, ^() { + typeof(self) strongSelf = weakSelf; + if ([path rangeOfString:kBITFileBaseString].location != NSNotFound) { + NSError *error = nil; + if (![[NSFileManager defaultManager] removeItemAtPath:path error:&error]) { + BITHockeyLogError(@"Error deleting file at path %@", path); + } + else { + BITHockeyLogDebug(@"INFO: Successfully deleted file at path %@", path); + [strongSelf.requestedBundlePaths removeObject:path]; + } + } else { + BITHockeyLogDebug(@"INFO: Empty path, nothing to delete"); + } + }); + +} + +- (void)giveBackRequestedFilePath:(NSString *)filePath { + __weak typeof(self) weakSelf = self; + dispatch_async(self.persistenceQueue, ^() { + typeof(self) strongSelf = weakSelf; + + [strongSelf.requestedBundlePaths removeObject:filePath]; + }); +} + +#pragma mark - Private + +- (nullable NSString *)fileURLForType:(BITPersistenceType)type { + + NSString *fileName = nil; + NSString *filePath; + + switch (type) { + case BITPersistenceTypeMetaData: { + fileName = kBITFileBaseStringMeta; + filePath = [self.appHockeySDKDirectoryPath stringByAppendingPathComponent:kBITMetaDataDirectory]; + break; + }; + case BITPersistenceTypeTelemetry: { + NSString *uuid = bit_UUID(); + fileName = [NSString stringWithFormat:@"%@%@", kBITFileBaseString, uuid]; + filePath = [self.appHockeySDKDirectoryPath stringByAppendingPathComponent:kBITTelemetryDirectory]; + break; + }; + } + + filePath = [filePath stringByAppendingPathComponent:fileName]; + + return filePath; +} + +/** + * Create directory structure if necessary and exclude it from iCloud backup + */ +- (void)createDirectoryStructureIfNeeded { + // Using the local variable looks unnecessary but it actually silences a static analyzer warning. + NSString *appHockeySDKDirectoryPath = [self appHockeySDKDirectoryPath]; + NSURL *appURL = [NSURL fileURLWithPath:appHockeySDKDirectoryPath]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (appURL) { + NSError *error = nil; + + // Create HockeySDK folder if needed + if (![fileManager createDirectoryAtURL:appURL withIntermediateDirectories:YES attributes:nil error:&error]) { + BITHockeyLogError(@"ERROR: %@", error.localizedDescription); + return; + } + + // Create metadata subfolder + NSURL *metaDataURL = [appURL URLByAppendingPathComponent:kBITMetaDataDirectory]; + if (![fileManager createDirectoryAtURL:metaDataURL withIntermediateDirectories:YES attributes:nil error:&error]) { + BITHockeyLogError(@"ERROR: %@", error.localizedDescription); + return; + } + + // Create telemetry subfolder + + //NOTE: createDirectoryAtURL:withIntermediateDirectories:attributes:error + //will return YES if the directory already exists and won't override anything. + //No need to check if the directory already exists. + NSURL *telemetryURL = [appURL URLByAppendingPathComponent:kBITTelemetryDirectory]; + if (![fileManager createDirectoryAtURL:telemetryURL withIntermediateDirectories:YES attributes:nil error:&error]) { + BITHockeyLogError(@"ERROR: %@", error.localizedDescription); + return; + } + + //Exclude HockeySDK folder from backup + if (![appURL setResourceValue:@YES + forKey:NSURLIsExcludedFromBackupKey + error:&error]) { + BITHockeyLogError(@"ERROR: Error excluding %@ from backup %@", appURL.lastPathComponent, error.localizedDescription); + } else { + BITHockeyLogDebug(@"INFO: Exclude %@ from backup", appURL); + } + + self.directorySetupComplete = YES; + } +} + +/** + * @returns the URL to the next file depending on the specified type. If there's no file, return nil. + */ +- (NSString *)nextURLOfType:(BITPersistenceType)type { + NSArray *fileNames = [self persistedFilesForType:type]; + if (fileNames && fileNames.count > 0) { + for (NSURL *filename in fileNames) { + NSString *absolutePath = filename.path; + if (![self.requestedBundlePaths containsObject:absolutePath]) { + return absolutePath; + } + } + } + return nil; +} + +- (NSArray *)persistedFilesForType: (BITPersistenceType)type { + NSString *directoryPath = [self folderPathForType:type]; + NSError *error = nil; + NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:[NSURL fileURLWithPath:directoryPath] + includingPropertiesForKeys:@[NSURLNameKey] + options:NSDirectoryEnumerationSkipsHiddenFiles + error:&error]; + return fileNames; +} + +- (NSString *)folderPathForType:(BITPersistenceType)type { + NSString *subFolder = @""; + switch (type) { + case BITPersistenceTypeTelemetry: { + subFolder = kBITTelemetryDirectory; + break; + } + case BITPersistenceTypeMetaData: { + subFolder = kBITMetaDataDirectory; + break; + } + } + return [self.appHockeySDKDirectoryPath stringByAppendingPathComponent:subFolder]; +} + +/** + * Send a BITHockeyPersistenceSuccessNotification to the main thread to notify observers that we have successfully saved a file + * This is typically used to trigger sending. + */ +- (void)sendBundleSavedNotification { + dispatch_async(dispatch_get_main_queue(), ^{ + BITHockeyLogDebug(@"Sending notification: %@", BITPersistenceSuccessNotification); + [[NSNotificationCenter defaultCenter] postNotificationName:BITPersistenceSuccessNotification + object:nil + userInfo:nil]; + }); +} + +- (NSString *)appHockeySDKDirectoryPath { + if (!_appHockeySDKDirectoryPath) { + NSString *appSupportPath = [[NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) lastObject] stringByStandardizingPath]; + if (appSupportPath) { + _appHockeySDKDirectoryPath = [appSupportPath stringByAppendingPathComponent:kBITHockeyDirectory]; + } + } + return _appHockeySDKDirectoryPath; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_METRICS */ + diff --git a/submodules/HockeySDK-iOS/Classes/BITPersistencePrivate.h b/submodules/HockeySDK-iOS/Classes/BITPersistencePrivate.h new file mode 100644 index 0000000000..9bba4df43e --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITPersistencePrivate.h @@ -0,0 +1,144 @@ +#import "HockeySDKFeatureConfig.h" + +#if HOCKEYSDK_FEATURE_METRICS + +#import "BITPersistence.h" + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +@interface BITPersistence () + +/** + * The BITPersistenceType determines if we have a bundle of meta data or telemetry that we want to safe. + */ +typedef NS_ENUM(NSInteger, BITPersistenceType) { + BITPersistenceTypeTelemetry = 0, + BITPersistenceTypeMetaData = 1 +}; + +/** + * Notification that will be send on the main thread to notifiy observers of a successfully saved bundle. + * This is typically used to trigger sending to the server. + */ +FOUNDATION_EXPORT NSString *const BITPersistenceSuccessNotification; + +///----------------------------------------------------------------------------- +/// @name Save/delete bundle of data +///----------------------------------------------------------------------------- + +/** + * A queue which makes file system operations thread safe. + */ +@property (nonatomic, strong) dispatch_queue_t persistenceQueue; + +/** + * Determines how many telemetry files can be on disk at a time. + */ +@property (nonatomic, assign) NSUInteger maxFileCount; + +@property (nonatomic, copy) NSString *appHockeySDKDirectoryPath; + +/** + * An array with all file paths, that have been requested by the sender. If the sender + * triggers a delete, the appropriate path should also be removed here. We keep to + * track of requested bundles to make sure that bundles don't get sent twice at the same + * time by differend http operations. + */ +@property (nonatomic, strong) NSMutableArray *requestedBundlePaths; + +/** + * Saves the bundle to disk. + * + * @param bundle the bundle, which should be saved to disk + */ +- (void)persistBundle:(NSData *)bundle; + +/** + * Saves the given dictionary to the session Ids file. + * + * @param metaData a dictionary consisting of unix timestamps and session ids + */ +- (void)persistMetaData:(NSDictionary *)metaData; + +/** + * Deletes the file for the given path. + * + * @param path the path of the file, which should be deleted + */ +- (void)deleteFileAtPath:(NSString *)path; + +/** + * Determines whether the persistence layer is able to write more files to disk. + * + * @return YES if the maxFileCount has not been reached, yet (otherwise NO). + */ +- (BOOL)isFreeSpaceAvailable; + +///----------------------------------------------------------------------------- +/// @name Get a bundle of saved data +///----------------------------------------------------------------------------- + +/** + * Returns the path for the next item to send. The requested path is reserved as long + * as leaveUpRequestedPath: gets called. + * + * @see giveBackRequestedPath: + * + * @return the path of the item, which should be sent next + */ +- (nullable NSString *)requestNextFilePath; + +/** + * Release a requested path. This method should be called after sending a file failed. + * + * @param filePath The path that should be available for sending again. + */ +- (void)giveBackRequestedFilePath:(NSString *)filePath; + +/** + * Return the json data for a given path + * + * @param filePath The path of the file + * + * @return a data object which contains telemetry data in json representation + */ +- (nullable NSData *)dataAtFilePath:(NSString *)filePath; + +/** + * Returns the content of the session Ids file. + * + * @return return a dictionary containing all session Ids + */ +- (NSDictionary *)metaData; + +///----------------------------------------------------------------------------- +/// @name Getting a path +///----------------------------------------------------------------------------- + +/** + * Returns a folder path for items of a given type. + * @param type The type + * @return a folder path for items of a given type + */ +- (NSString *)folderPathForType:(BITPersistenceType)type; + +///----------------------------------------------------------------------------- +/// @name Getting a path +///----------------------------------------------------------------------------- + +/** + * Creates the path for a file + * The filename includes the timestamp. + * + * @param type The type that you want the fileURL for +*/ +- (nullable NSString *)fileURLForType:(BITPersistenceType)type; + +- (void)createDirectoryStructureIfNeeded; + +#endif /* HOCKEYSDK_FEATURE_METRICS */ + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/HockeySDK-iOS/Classes/BITRectangleImageAnnotation.h b/submodules/HockeySDK-iOS/Classes/BITRectangleImageAnnotation.h new file mode 100644 index 0000000000..024957998a --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITRectangleImageAnnotation.h @@ -0,0 +1,33 @@ +/* + * Author: Moritz Haarmann + * + * 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 "BITImageAnnotation.h" + +@interface BITRectangleImageAnnotation : BITImageAnnotation + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITRectangleImageAnnotation.m b/submodules/HockeySDK-iOS/Classes/BITRectangleImageAnnotation.m new file mode 100644 index 0000000000..ed20737dc7 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITRectangleImageAnnotation.m @@ -0,0 +1,91 @@ +/* + * Author: Moritz Haarmann + * + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_FEEDBACK + +#import "BITRectangleImageAnnotation.h" + +@interface BITRectangleImageAnnotation() + +@property (nonatomic, strong) CAShapeLayer *shapeLayer; +@property (nonatomic, strong) CAShapeLayer *strokeLayer; + +@end + +@implementation BITRectangleImageAnnotation + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.shapeLayer = [CAShapeLayer layer]; + self.shapeLayer.strokeColor = [UIColor redColor].CGColor; + self.shapeLayer.lineWidth = 5; + self.shapeLayer.fillColor = [UIColor clearColor].CGColor; + + self.strokeLayer = [CAShapeLayer layer]; + self.strokeLayer.strokeColor = [UIColor whiteColor].CGColor; + self.strokeLayer.lineWidth = 10; + self.strokeLayer.fillColor = [UIColor clearColor].CGColor; + [self.layer addSublayer:self.strokeLayer]; + + [self.layer addSublayer:self.shapeLayer]; + + } + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + self.shapeLayer.frame = self.bounds; + self.shapeLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10].CGPath; + + + self.strokeLayer.frame = self.bounds; + self.strokeLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10].CGPath; + + CGFloat lineWidth = MAX(self.frame.size.width / 10,10); + + [CATransaction begin]; + [CATransaction setAnimationDuration:0]; + self.strokeLayer.lineWidth = lineWidth/(CGFloat)1.5; + self.shapeLayer.lineWidth = lineWidth /(CGFloat)3.0; + + [CATransaction commit]; +} + +- (BOOL)resizable { + return YES; +} + + +@end + +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/submodules/HockeySDK-iOS/Classes/BITSender.h b/submodules/HockeySDK-iOS/Classes/BITSender.h new file mode 100644 index 0000000000..8ebe6e81d4 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITSender.h @@ -0,0 +1,129 @@ +#import +#import "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_METRICS + +@class BITPersistence; + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +/** + * Utility class that's responsible for sending a bundle of data to the server + */ +@interface BITSender : NSObject + +/** + * Notification that will be send on the main thread to notifiy observers of finish sending data. + */ +FOUNDATION_EXPORT NSString *const BITSenderFinishSendingDataNotification; + +///----------------------------------------------------------------------------- +/// @name Initialize instance +///----------------------------------------------------------------------------- + +/** + * Initializes a sender instance with a given persistence object. + * + * @param persistence used for loading files before sending them out + * @param serverURL the endpoint URL for telemetry data + * @return an initialized sender instance + */ +- (instancetype)initWithPersistence:(BITPersistence *)persistence serverURL:(NSURL *)serverURL; + +/** + * A queue which is used to handle completion blocks. + */ +@property (nonatomic, strong) dispatch_queue_t senderTasksQueue; + +/** + * The endpoint url of the telemetry server. + */ +@property (nonatomic, copy) NSString *endpointPath; + +/** + * The max number of request that can run at a time. + */ +@property (nonatomic, assign) NSUInteger maxRequestCount; + +/** + * The number of requests that are currently running. + */ +@property (atomic, assign) NSUInteger runningRequestsCount; + +/** + * BaseURL to which relative paths are appended. + */ +@property (nonatomic, strong, readonly) NSURL *serverURL; + +/** + * The persistence instance used for loading files before sending them out. + */ +@property (nonatomic, strong, readonly) BITPersistence *persistence; + +///----------------------------------------------------------------------------- +/// @name Sending data +///----------------------------------------------------------------------------- + +/** + * Creates a request for the given data and forwards that in order to send it out. + * + * @param data the telemetry data which should be sent + * @param filePath a reference of filePath to the file which should be sent (needed to delete it after sending) + */ +- (void)sendData:(NSData *)data withFilePath:(NSString * )filePath; + +/** + * Triggers sending the saved data on a background thread. Does nothing if nothing has been persisted, yet. This method should be called on app start. + */ +- (void)sendSavedDataAsync; + +/** + * Triggers sending the saved data. + */ +- (void)sendSavedData; + +/** + * Creates a HTTP operation/session task and puts it to the queue. + * + * @param request a request for sending a data object to the telemetry server + * @param path path to the file which should be sent + */ +- (void)sendRequest:(NSURLRequest *)request filePath:(NSString *)path; + +/** + * Deletes or unblocks sent file according to the given response code. + * + * @param statusCode the status code of the response + * @param responseData the data of the response + * @param filePath the path of the file which content has been sent to the server + * @param error an error object sent from the server + */ +- (void)handleResponseWithStatusCode:(NSInteger)statusCode responseData:(NSData *)responseData filePath:(NSString *)filePath error:(NSError *)error; + +///----------------------------------------------------------------------------- +/// @name Helper +///----------------------------------------------------------------------------- + +/** + * Returns a request for sending data to the telemetry sender. + * + * @param data the data which should be sent + * + * @return a request which contains the given data + */ +- (NSURLRequest *)requestForData:(NSData *)data; + +/** + * Returns if data should be deleted based on a given status code. + * + * @param statusCode the status code which is part of the response object + * + * @return YES if data should be deleted, NO if the payload should be sent at a later time again. + */ +- (BOOL)shouldDeleteDataWithStatusCode:(NSInteger)statusCode; + +@end +NS_ASSUME_NONNULL_END + +#endif /* HOCKEYSDK_FEATURE_METRICS */ diff --git a/submodules/HockeySDK-iOS/Classes/BITSender.m b/submodules/HockeySDK-iOS/Classes/BITSender.m new file mode 100644 index 0000000000..0b6627cd50 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITSender.m @@ -0,0 +1,209 @@ +#import "BITSender.h" + +#if HOCKEYSDK_FEATURE_METRICS + +#import "BITPersistencePrivate.h" +#import "BITChannelPrivate.h" +#import "BITGZIP.h" +#import "HockeySDKPrivate.h" +#import "BITHockeyHelper.h" + +NSString *const BITSenderFinishSendingDataNotification = @"BITSenderFinishSendingDataNotification"; + +static char const *kBITSenderTasksQueueString = "net.hockeyapp.sender.tasksQueue"; +static NSUInteger const BITDefaultRequestLimit = 10; + +@interface BITSender () + +@property (nonatomic, strong) NSURLSession *session; + +@property (nonatomic, weak, nullable) id persistenceSuccessObserver; +@property (nonatomic, weak, nullable) id channelBlockedObserver; + +@end + +@implementation BITSender + +@synthesize runningRequestsCount = _runningRequestsCount; +@synthesize persistence = _persistence; + +#pragma mark - Initialize instance + +- (instancetype)initWithPersistence:(nonnull BITPersistence *)persistence serverURL:(nonnull NSURL *)serverURL { + if ((self = [super init])) { + _senderTasksQueue = dispatch_queue_create(kBITSenderTasksQueueString, DISPATCH_QUEUE_CONCURRENT); + _maxRequestCount = BITDefaultRequestLimit; + _serverURL = serverURL; + _persistence = persistence; + [self registerObservers]; + } + return self; +} + +- (void)dealloc { + [self unregisterObservers]; +} + +#pragma mark - Handle persistence events + +- (void)registerObservers { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + __weak typeof(self) weakSelf = self; + + if (nil == self.persistenceSuccessObserver) { + self.persistenceSuccessObserver = + [center addObserverForName:BITPersistenceSuccessNotification + object:nil + queue:nil + usingBlock:^(NSNotification __unused *notification) { + typeof(self) strongSelf = weakSelf; + [strongSelf sendSavedDataAsync]; + }]; + } + if (nil == self.channelBlockedObserver) { + self.channelBlockedObserver = + [center addObserverForName:BITChannelBlockedNotification + object:nil + queue:nil + usingBlock:^(NSNotification __unused *notification) { + typeof(self) strongSelf = weakSelf; + [strongSelf sendSavedDataAsync]; + }]; + } +} + +- (void)unregisterObservers { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + id persistenceSuccessObserver = self.persistenceSuccessObserver; + if(persistenceSuccessObserver) { + [center removeObserver:persistenceSuccessObserver]; + self.persistenceSuccessObserver = nil; + } + id channelBlockedObserver = self.channelBlockedObserver; + if(channelBlockedObserver) { + [center removeObserver:channelBlockedObserver]; + self.channelBlockedObserver = nil; + } +} + +#pragma mark - Sending + +- (void)sendSavedDataAsync { + dispatch_async(self.senderTasksQueue, ^{ + [self sendSavedData]; + }); +} + +- (void)sendSavedData { + @synchronized(self) { + if (self.runningRequestsCount < self.maxRequestCount) { + self.runningRequestsCount++; + BITHockeyLogDebug(@"INFO: Create new sender thread. Current count is %ld", (long) self.runningRequestsCount); + } else { + return; + } + } + + NSString *filePath = [self.persistence requestNextFilePath]; + NSData *data = [self.persistence dataAtFilePath:filePath]; + [self sendData:data withFilePath:filePath]; +} + +- (void)sendData:(nonnull NSData *)data withFilePath:(nonnull NSString *)filePath { + if (data && data.length > 0) { + NSData *gzippedData = [data bit_gzippedData]; + NSURLRequest *request = [self requestForData:gzippedData]; + + BITHockeyLogVerbose(@"VERBOSE: Sending data:\n%@", [[NSString alloc] initWithData:data encoding:kCFStringEncodingUTF8]); + [self sendRequest:request filePath:filePath]; + } else { + self.runningRequestsCount--; + BITHockeyLogDebug(@"INFO: Close sender thread due empty package. Current count is %ld", (long) self.runningRequestsCount); + } +} + +- (void)sendRequest:(nonnull NSURLRequest *)request filePath:(nonnull NSString *)path { + if (!path || !request) { + return; + } + NSURLSession *session = self.session; + NSURLSessionDataTask *task = [session dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response; + NSInteger statusCode = httpResponse.statusCode; + [self handleResponseWithStatusCode:statusCode responseData:data filePath:path error:error]; + }]; + [task resume]; +} + +- (void)handleResponseWithStatusCode:(NSInteger)statusCode responseData:(nonnull NSData *)responseData filePath:(nonnull NSString *)filePath error:(nonnull NSError *)error { + self.runningRequestsCount--; + BITHockeyLogDebug(@"INFO: Close sender thread due incoming response. Current count is %ld", (long) self.runningRequestsCount); + + if (responseData && (responseData.length > 0) && [self shouldDeleteDataWithStatusCode:statusCode]) { + //we delete data that was either sent successfully or if we have a non-recoverable error + BITHockeyLogDebug(@"INFO: Sent data with status code: %ld", (long) statusCode); + BITHockeyLogDebug(@"INFO: Response data:\n%@", [NSJSONSerialization JSONObjectWithData:responseData options:0 error:nil]); + [self.persistence deleteFileAtPath:filePath]; + [self sendSavedData]; + } else { + BITHockeyLogError(@"ERROR: Sending telemetry data failed"); + BITHockeyLogError(@"Error description: %@", error.localizedDescription); + [self.persistence giveBackRequestedFilePath:filePath]; + } + + if (self.runningRequestsCount == 0) { + [self sendSenderFinishSendingDataNotification]; + } +} + +- (void)sendSenderFinishSendingDataNotification { + dispatch_async(dispatch_get_main_queue(), ^{ + BITHockeyLogDebug(@"Sending notification: %@", BITSenderFinishSendingDataNotification); + [[NSNotificationCenter defaultCenter] postNotificationName:BITSenderFinishSendingDataNotification + object:nil + userInfo:nil]; + }); +} + +#pragma mark - Helper + +- (NSURLRequest *)requestForData:(nonnull NSData *)data { + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.serverURL]; + request.HTTPMethod = @"POST"; + + request.HTTPBody = data; + request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + + NSDictionary *headers = @{@"Charset" : @"UTF-8", + @"Content-Encoding" : @"gzip", + @"Content-Type" : @"application/x-json-stream", + @"Accept-Encoding" : @"gzip"}; + [request setAllHTTPHeaderFields:headers]; + + return request; +} + +//some status codes represent recoverable error codes +//we try sending again some point later +- (BOOL)shouldDeleteDataWithStatusCode:(NSInteger)statusCode { + NSArray *recoverableStatusCodes = @[@429, @408, @500, @503, @511]; + + return ![recoverableStatusCodes containsObject:@(statusCode)]; +} + +#pragma mark - Getter/Setter + +- (NSURLSession *)session { + if (!_session) { + NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + _session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; + } + return _session; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_METRICS */ + diff --git a/submodules/HockeySDK-iOS/Classes/BITSession.h b/submodules/HockeySDK-iOS/Classes/BITSession.h new file mode 100644 index 0000000000..04843ee2d3 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITSession.h @@ -0,0 +1,9 @@ +#import "BITTelemetryObject.h" + +@interface BITSession : BITTelemetryObject + +@property (nonatomic, copy) NSString *sessionId; +@property (nonatomic, copy) NSString *isFirst; +@property (nonatomic, copy) NSString *isNew; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITSession.m b/submodules/HockeySDK-iOS/Classes/BITSession.m new file mode 100644 index 0000000000..e8871bdd38 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITSession.m @@ -0,0 +1,45 @@ +#import "BITSession.h" + +/// Data contract class for type Session. +@implementation BITSession + +/// +/// Adds all members of this class to a dictionary +/// @returns dictionary to which the members of this class will be added. +/// +- (NSDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary].mutableCopy; + if (self.sessionId != nil) { + [dict setObject:self.sessionId forKey:@"ai.session.id"]; + } + if (self.isFirst != nil) { + [dict setObject:self.isFirst forKey:@"ai.session.isFirst"]; + } + if (self.isNew != nil) { + [dict setObject:self.isNew forKey:@"ai.session.isNew"]; + } + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if(self) { + _sessionId = [coder decodeObjectForKey:@"self.sessionId"]; + _isFirst = [coder decodeObjectForKey:@"self.isFirst"]; + _isNew = [coder decodeObjectForKey:@"self.isNew"]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.sessionId forKey:@"self.sessionId"]; + [coder encodeObject:self.isFirst forKey:@"self.isFirst"]; + [coder encodeObject:self.isNew forKey:@"self.isNew"]; +} + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITSessionState.h b/submodules/HockeySDK-iOS/Classes/BITSessionState.h new file mode 100755 index 0000000000..10c9368b53 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITSessionState.h @@ -0,0 +1,8 @@ +#import +/// Enum class for type SessionState. +typedef NS_ENUM(NSInteger, BITSessionState) { + BITSessionState_start = 0, + + BITSessionState_end = 1, + +}; diff --git a/submodules/HockeySDK-iOS/Classes/BITSessionStateData.h b/submodules/HockeySDK-iOS/Classes/BITSessionStateData.h new file mode 100755 index 0000000000..63192c92f8 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITSessionStateData.h @@ -0,0 +1,10 @@ +#import "BITDomain.h" +#import "BITSessionState.h" + +@interface BITSessionStateData : BITDomain + +@property (nonatomic, copy, readonly) NSString *envelopeTypeName; +@property (nonatomic, copy, readonly) NSString *dataTypeName; +@property (nonatomic, assign) BITSessionState state; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITSessionStateData.m b/submodules/HockeySDK-iOS/Classes/BITSessionStateData.m new file mode 100755 index 0000000000..ee755f25b7 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITSessionStateData.m @@ -0,0 +1,49 @@ +#import "BITSessionStateData.h" + +/// Data contract class for type SessionStateData. +@implementation BITSessionStateData +@synthesize envelopeTypeName = _envelopeTypeName; +@synthesize dataTypeName = _dataTypeName; +@synthesize version = _version; + +/// Initializes a new instance of the class. +- (instancetype)init { + if((self = [super init])) { + _envelopeTypeName = @"Microsoft.ApplicationInsights.SessionState"; + _dataTypeName = @"SessionStateData"; + _version = @2; + _state = BITSessionState_start; + } + return self; +} + +/// +/// Adds all members of this class to a dictionary +/// @returns dictionary to which the members of this class will be added. +/// +- (NSDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary].mutableCopy; + [dict setObject:[NSNumber numberWithInt:(int)self.state] forKey:@"state"]; + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if(self) { + _envelopeTypeName =[coder decodeObjectForKey:@"envelopeTypeName"]; + _dataTypeName = [coder decodeObjectForKey:@"dataTypeName"]; + _state = (BITSessionState)[coder decodeIntForKey:@"self.state"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.envelopeTypeName forKey:@"envelopeTypeName"]; + [coder encodeObject:self.dataTypeName forKey:@"dataTypeName"]; + [coder encodeInt:self.state forKey:@"self.state"]; +} + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITStoreButton.h b/submodules/HockeySDK-iOS/Classes/BITStoreButton.h new file mode 100644 index 0000000000..d44f3bb216 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITStoreButton.h @@ -0,0 +1,84 @@ +/* + * Author: Andreas Linde + * Peter Steinberger + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011-2012 Peter Steinberger. + * 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 + +// defines a button action set (data container) +@interface BITStoreButtonData : NSObject + ++ (id)dataWithLabel:(NSString*)aLabel enabled:(BOOL)flag; + +@property (nonatomic, copy) NSString *label; +@property (nonatomic, assign, getter=isEnabled) BOOL enabled; + +@end + + +@class BITStoreButton; +@protocol BITStoreButtonDelegate +- (void)storeButtonFired:(BITStoreButton *)button; +@end + +/** + * Button style depending on the iOS version + */ +typedef NS_ENUM(NSUInteger, BITStoreButtonStyle) { + /** + * Draw buttons in the iOS 7 style + */ + BITStoreButtonStyleOS7 = 0 +}; + + +// Simulate the Payment Button from the AppStore +// The interface is flexible, so there is now fixed order +@interface BITStoreButton : UIButton + +- (instancetype)initWithFrame:(CGRect)frame; +- (instancetype)initWithPadding:(CGPoint)padding style:(BITStoreButtonStyle)style; + +// action delegate +@property (nonatomic, weak) id buttonDelegate; + +// change the button layer +@property (nonatomic, strong) BITStoreButtonData *buttonData; +- (void)setButtonData:(BITStoreButtonData *)aButtonData animated:(BOOL)animated; + +// align helper +@property (nonatomic, assign) CGPoint customPadding; + +// align helper +@property (nonatomic, assign) BITStoreButtonStyle style; + + +- (void)alignToSuperview; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITStoreButton.m b/submodules/HockeySDK-iOS/Classes/BITStoreButton.m new file mode 100644 index 0000000000..8ac67a3e22 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITStoreButton.m @@ -0,0 +1,266 @@ +/* + * Author: Andreas Linde + * Peter Steinberger + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011-2012 Peter Steinberger. + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_UPDATES + +#import "BITStoreButton.h" +#import "HockeySDKPrivate.h" +#import + +#define BIT_MIN_HEIGHT 25.0 +#define BIT_MAX_WIDTH 120.0 +#define BIT_PADDING 12.0 +#define kDefaultButtonAnimationTime 0.25 + + +@implementation BITStoreButtonData + +#pragma mark - NSObject + +- (instancetype)initWithLabel:(NSString*)aLabel enabled:(BOOL)flag { + if ((self = [super init])) { + self.label = aLabel; + self.enabled = flag; + } + return self; +} + ++ (id)dataWithLabel:(NSString*)aLabel enabled:(BOOL)flag { + return [[[self class] alloc] initWithLabel:aLabel enabled:flag]; +} + +@end + +@interface BITStoreButton () + +@property (nonatomic, strong) CALayer *defaultBorderLayer; +@property (nonatomic, strong) CALayer *inActiveBorderLayer; + +@end + +@implementation BITStoreButton + +#pragma mark - private + +- (void)buttonPressed:(id) __unused sender { + [self.buttonDelegate storeButtonFired:self]; +} + +- (void)animationDidStop:(NSString *) __unused animationID finished:(NSNumber *)finished context:(void *) __unused context { + // show text again, but only if animation did finish (or else another animation is on the way) + if ([finished boolValue]) { + [self setTitle:self.buttonData.label forState:UIControlStateNormal]; + } +} + +- (void)updateButtonAnimated:(BOOL)animated { + if (animated) { + // hide text, then start animation + [self setTitle:@"" forState:UIControlStateNormal]; + [UIView beginAnimations:@"storeButtonUpdate" context:nil]; + [UIView setAnimationBeginsFromCurrentState:YES]; + [UIView setAnimationDuration:kDefaultButtonAnimationTime]; + [UIView setAnimationDelegate:self]; + [UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)]; + } else { + [self setTitle:self.buttonData.label forState:UIControlStateNormal]; + } + + self.enabled = self.buttonData.isEnabled; + + // show white or gray text, depending on the state + if (self.buttonData.isEnabled) { + [self setTitleColor:BIT_RGBCOLOR(35, 111, 251) forState:UIControlStateNormal]; + [self.defaultBorderLayer setHidden:NO]; + [self.inActiveBorderLayer setHidden:YES]; + } else { + [self setTitleColor:BIT_RGBCOLOR(148, 150, 151) forState:UIControlStateNormal]; + if (self.style == BITStoreButtonStyleOS7) { + [self.defaultBorderLayer setHidden:YES]; + [self.inActiveBorderLayer setHidden:NO]; + } + } + + // calculate optimal new size + CGSize sizeThatFits = [self sizeThatFits:CGSizeZero]; + + // move sublayer (can't be animated explcitely) + for (CALayer *aLayer in self.layer.sublayers) { + [CATransaction begin]; + + if (animated) { + [CATransaction setAnimationDuration:kDefaultButtonAnimationTime]; + [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]]; + } else { + // frame is calculated and explicitly animated. so we absolutely need kCATransactionDisableActions + [CATransaction setValue:[NSNumber numberWithBool:YES] forKey:kCATransactionDisableActions]; + } + + CGRect newFrame = aLayer.frame; + newFrame.size.width = sizeThatFits.width; + aLayer.frame = newFrame; + + [CATransaction commit]; + } + + // set outer frame changes + self.titleEdgeInsets = UIEdgeInsetsMake(2.0, self.titleEdgeInsets.left, 0.0, 0.0); + [self alignToSuperview]; + + if (animated) { + [UIView commitAnimations]; + } +} + +- (void)alignToSuperview { + [self sizeToFit]; + if (self.superview) { + CGRect cr = self.frame; + cr.origin.y = self.customPadding.y; + cr.origin.x = self.superview.frame.size.width - cr.size.width - self.customPadding.x * 2; + self.frame = cr; + } +} + + +#pragma mark - NSObject + +- (instancetype)initWithFrame:(CGRect)frame { + if ((self = [super initWithFrame:frame])) { + self.layer.needsDisplayOnBoundsChange = YES; + + // setup title label + [self.titleLabel setFont:[UIFont boldSystemFontOfSize:13.0]]; + + // register for touch events + [self addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside]; + + [self bringSubviewToFront:(UILabel *)self.titleLabel]; + } + return self; +} + +- (instancetype)initWithPadding:(CGPoint)padding style:(BITStoreButtonStyle)style { + CGRect frame = CGRectMake(0, 0, 40, BIT_MIN_HEIGHT); + if ((self = [self initWithFrame:frame])) { + _customPadding = padding; + _style = style; + + // border layers for more sex! + _defaultBorderLayer = [CALayer layer]; + _defaultBorderLayer.borderColor = [BIT_RGBCOLOR(35, 111, 251) CGColor]; + _defaultBorderLayer.borderWidth = 1.0; + _defaultBorderLayer.frame = CGRectMake(0.0, 0.0, CGRectGetWidth(frame), CGRectGetHeight(frame)); + _defaultBorderLayer.cornerRadius = 2.5; + _defaultBorderLayer.needsDisplayOnBoundsChange = YES; + [self.layer addSublayer:_defaultBorderLayer]; + + if (style == BITStoreButtonStyleOS7) { + _inActiveBorderLayer = [CALayer layer]; + _inActiveBorderLayer.borderColor = [BIT_RGBCOLOR(148, 150, 151) CGColor]; + _inActiveBorderLayer.borderWidth = 1.0; + _inActiveBorderLayer.frame = CGRectMake(0.0, 0.0, CGRectGetWidth(frame), CGRectGetHeight(frame)); + _inActiveBorderLayer.cornerRadius = 2.5; + _inActiveBorderLayer.needsDisplayOnBoundsChange = YES; + [self.layer addSublayer:_inActiveBorderLayer]; + [_inActiveBorderLayer setHidden:YES]; + } + + [self bringSubviewToFront:(UILabel *)self.titleLabel]; + } + return self; +} + + + +#pragma mark - UIView + +- (CGSize)sizeThatFits:(CGSize) __unused size { + CGSize constr = (CGSize){.height = self.frame.size.height, .width = BIT_MAX_WIDTH}; + CGSize newSize; + + if ([self.buttonData.label respondsToSelector:@selector(boundingRectWithSize:options:attributes:context:)]) { + CGRect calculatedRect = [self.buttonData.label boundingRectWithSize:constr + options:NSStringDrawingUsesFontLeading + attributes:@{NSFontAttributeName:(id)self.titleLabel.font} + context:nil]; + newSize = calculatedRect.size; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + newSize = [self.buttonData.label sizeWithFont:self.titleLabel.font + constrainedToSize:constr + lineBreakMode:NSLineBreakByTruncatingMiddle]; +#pragma clang diagnostic pop + } + + CGFloat newWidth = newSize.width + ((CGFloat)BIT_PADDING * 2); + CGFloat newHeight = (CGFloat)BIT_MIN_HEIGHT > newSize.height ? (CGFloat)BIT_MIN_HEIGHT : newSize.height; + + CGSize sizeThatFits = CGSizeMake(newWidth, newHeight); + return sizeThatFits; +} + +- (void)setFrame:(CGRect)aRect { + [super setFrame:aRect]; + + // copy frame changes to sublayers (but watch out for NaN's) + for (CALayer *aLayer in self.layer.sublayers) { + CGRect rect = aLayer.frame; + rect.size.width = self.frame.size.width; + rect.size.height = self.frame.size.height; + aLayer.frame = rect; + [aLayer layoutIfNeeded]; + } +} + + +#pragma mark - Properties + +- (void)setButtonData:(BITStoreButtonData *)aButtonData { + [self setButtonData:aButtonData animated:NO]; +} + +- (void)setButtonData:(BITStoreButtonData *)aButtonData animated:(BOOL)animated { + if (self.buttonData != aButtonData) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + _buttonData = aButtonData; +#pragma clang diagnostic pop + } + + [self updateButtonAnimated:animated]; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_UPDATES */ diff --git a/submodules/HockeySDK-iOS/Classes/BITStoreUpdateManager.h b/submodules/HockeySDK-iOS/Classes/BITStoreUpdateManager.h new file mode 100644 index 0000000000..b7b01d0233 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITStoreUpdateManager.h @@ -0,0 +1,186 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2013-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 +#import "BITHockeyBaseManager.h" + + +/** + * Defines the update check intervals + */ +typedef NS_ENUM(NSInteger, BITStoreUpdateSetting) { + /** + * Check every day + */ + BITStoreUpdateCheckDaily = 0, + /** + * Check every week + */ + BITStoreUpdateCheckWeekly = 1, + /** + * Check manually + */ + BITStoreUpdateCheckManually = 2 +}; + +@protocol BITStoreUpdateManagerDelegate; + +/** + The store update manager module. + + This is the HockeySDK module for handling app updates when having your app released in the App Store. + By default the module uses the current users locale to define the app store to check for updates. You + can modify this using the `countryCode` property. See the property documentation for details on its usage. + + When an update is detected, this module will show an alert asking the user if he/she wants to update or + ignore this version. If update was chosen, it will open the apps page in the app store app. + + You need to enable this module using `[BITHockeyManager enableStoreUpdateManager]` if you want to use this + feature. By default this module is disabled! + + When this module is enabled and **NOT** running in an App Store build/environment, it won't do any checks! + + The `BITStoreUpdateManagerDelegate` protocol informs the app about new detected app versions. + + @warning This module can **NOT** check if the current device and OS version match the minimum requirements of + the new app version! + + */ + +@interface BITStoreUpdateManager : BITHockeyBaseManager + +///----------------------------------------------------------------------------- +/// @name Update Checking +///----------------------------------------------------------------------------- + +/** + When to check for new updates. + + Defines when a the SDK should check if there is a new update available on the + server. This must be assigned one of the following, see `BITStoreUpdateSetting`: + + - `BITStoreUpdateCheckDaily`: Once a day + - `BITStoreUpdateCheckWeekly`: Once a week + - `BITStoreUpdateCheckManually`: Manually + + **Default**: BITStoreUpdateCheckWeekly + + @warning When setting this to `BITStoreUpdateCheckManually` you need to either + invoke the update checking process yourself with `checkForUpdate` somehow, e.g. by + proving an update check button for the user or integrating the Update View into your + user interface. + @see BITStoreUpdateSetting + @see countryCode + @see checkForUpdateOnLaunch + @see checkForUpdate + */ +@property (nonatomic, assign) BITStoreUpdateSetting updateSetting; + + +/** + Defines the store country the app is always available in, otherwise uses the users locale + + If this value is not defined, then it uses the device country if the current locale. + + If you are pre-defining a country and are releasing a new version on a specific date, + it can happen that users get an alert but the update is not yet available in their country! + + But if a user downloaded the app from another appstore than the locale is set and the app is not + available in the locales app store, then the user will never receive an update notification! + + More information about possible country codes is available here: http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + + @see updateSetting + @see checkForUpdateOnLaunch + @see checkForUpdate + */ +@property (nonatomic, copy) NSString *countryCode; + + +/** + Flag that determines whether the automatic update checks should be done. + + If this is enabled the update checks will be performed automatically depending on the + `updateSetting` property. If this is disabled the `updateSetting` property will have + no effect, and checking for updates is totally up to be done by yourself. + + *Default*: _YES_ + + @warning When setting this to `NO` you need to invoke update checks yourself! + @see updateSetting + @see countryCode + @see checkForUpdate + */ +@property (nonatomic, assign, getter=isCheckingForUpdateOnLaunch) BOOL checkForUpdateOnLaunch; + + +///----------------------------------------------------------------------------- +/// @name User Interface +///----------------------------------------------------------------------------- + + +/** + Flag that determines if the integrated update alert should be used + + If enabled, the integrated UIAlert based update notification will be used to inform + the user about a new update being available in the App Store. + + If disabled, you need to implement the `BITStoreUpdateManagerDelegate` protocol with + the method `[BITStoreUpdateManagerDelegate detectedUpdateFromStoreUpdateManager:newVersion:storeURL:]` + to be notified about new version and proceed yourself. + The manager will consider this identical to an `Ignore` user action using the alert + and not inform about this particular version any more, unless the app is updated + and this very same version shows up at a later time again as a new version. + + *Default*: _YES_ + + @warning If the HockeySDKResources bundle is missing in the application package, then the internal + update alert is also disabled and be treated identical to manually disabling this + property. + @see updateSetting + */ +@property (nonatomic, assign, getter=isUpdateUIEnabled) BOOL updateUIEnabled; + +///----------------------------------------------------------------------------- +/// @name Manual update checking +///----------------------------------------------------------------------------- + +/** + Check for an update + + Call this to trigger a check if there is a new update available on the HockeyApp servers. + + @see updateSetting + @see countryCode + @see checkForUpdateOnLaunch + */ +- (void)checkForUpdate; + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITStoreUpdateManager.m b/submodules/HockeySDK-iOS/Classes/BITStoreUpdateManager.m new file mode 100644 index 0000000000..0626805d71 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITStoreUpdateManager.m @@ -0,0 +1,504 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2013-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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_STORE_UPDATES + +#import + +#import "HockeySDKPrivate.h" +#import "BITHockeyHelper.h" +#import "BITHockeyHelper+Application.h" + +#import "BITHockeyBaseManagerPrivate.h" +#import "BITStoreUpdateManagerPrivate.h" + +@interface BITStoreUpdateManager () + +@property (nonatomic, copy) NSString *latestStoreVersion; +@property (nonatomic, copy) NSString *appStoreURLString; +@property (nonatomic, copy) NSString *currentUUID; +@property (nonatomic) BOOL updateAlertShowing; +@property (nonatomic) BOOL lastCheckFailed; +@property (nonatomic, weak) id appDidBecomeActiveObserver; +@property (nonatomic, weak) id networkDidBecomeReachableObserver; + +@end + +@implementation BITStoreUpdateManager + +#pragma mark - private + +- (void)reportError:(NSError *)error { + BITHockeyLogError(@"ERROR: %@", [error localizedDescription]); + self.lastCheckFailed = YES; +} + + +- (void)didBecomeActiveActions { + if ([self shouldCancelProcessing]) return; + + if ([self isCheckingForUpdateOnLaunch] && [self shouldAutoCheckForUpdates]) { + [self performSelector:@selector(checkForUpdateDelayed) withObject:nil afterDelay:1.0]; + } +} + +#pragma mark - Observers + +- (void) registerObservers { + __weak typeof(self) weakSelf = self; + if(nil == self.appDidBecomeActiveObserver) { + self.appDidBecomeActiveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf didBecomeActiveActions]; + }]; + } + if(nil == self.networkDidBecomeReachableObserver) { + self.networkDidBecomeReachableObserver = [[NSNotificationCenter defaultCenter] addObserverForName:BITHockeyNetworkDidBecomeReachableNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf didBecomeActiveActions]; + }]; + } +} + +- (void) unregisterObservers { + id strongAppDidBecomeActiveObserver = self.appDidBecomeActiveObserver; + id strongNetworkDidBecomeReachableObserver = self.networkDidBecomeReachableObserver; + if(strongAppDidBecomeActiveObserver) { + [[NSNotificationCenter defaultCenter] removeObserver:strongAppDidBecomeActiveObserver]; + self.appDidBecomeActiveObserver = nil; + } + if(strongNetworkDidBecomeReachableObserver) { + [[NSNotificationCenter defaultCenter] removeObserver:strongNetworkDidBecomeReachableObserver]; + self.networkDidBecomeReachableObserver = nil; + } +} + + +#pragma mark - Init + +- (instancetype)init { + if ((self = [super init])) { + _checkInProgress = NO; + _updateAvailable = NO; + _lastCheckFailed = NO; + _enableStoreUpdateManager = NO; + _updateAlertShowing = NO; + _updateUIEnabled = YES; + _latestStoreVersion = nil; + _appStoreURLString = nil; + _currentUUID = [[self executableUUID] copy]; + _countryCode = nil; + + _mainBundle = [NSBundle mainBundle]; + _currentLocale = [NSLocale currentLocale]; + _userDefaults = [NSUserDefaults standardUserDefaults]; + + // set defaults + self.checkForUpdateOnLaunch = YES; + self.updateSetting = BITStoreUpdateCheckWeekly; + + if (!BITHockeyBundle()) { + BITHockeyLogWarning(@"[HockeySDK] WARNING: %@ is missing, built in UI is deactivated!", BITHOCKEYSDK_BUNDLE); + } + } + return self; +} + +- (void)dealloc { + [self unregisterObservers]; +} + + +#pragma mark - Version + +- (NSString *)lastStoreVersion { + NSString *versionString = nil; + + if ([self.userDefaults objectForKey:kBITStoreUpdateLastStoreVersion]) { + // get the last saved version string from the app store + versionString = [self.userDefaults objectForKey:kBITStoreUpdateLastStoreVersion]; + } + + // if there is a UUID saved which doesn't match the current binary UUID + // then there is possibly a newer version in the store + NSString *lastSavedUUID = nil; + if ([self.userDefaults objectForKey:kBITStoreUpdateLastUUID]) { + lastSavedUUID = [self.userDefaults objectForKey:kBITStoreUpdateLastUUID]; + + if (lastSavedUUID && [lastSavedUUID length] > 0 && ![lastSavedUUID isEqualToString:self.currentUUID]) { + // the UUIDs don't match, store the new one + [self.userDefaults setObject:self.currentUUID forKey:kBITStoreUpdateLastUUID]; + + if (versionString) { + // a new version has been installed, reset everything + // so we set versionString to nil to simulate that this is the very run + [self.userDefaults removeObjectForKey:kBITStoreUpdateLastStoreVersion]; + versionString = nil; + } + } + } + + return versionString; +} + +- (BOOL)hasNewVersion:(NSDictionary *)dictionary { + self.lastCheckFailed = YES; + + NSString *lastStoreVersion = [self lastStoreVersion]; + + if ([[dictionary objectForKey:@"results"] isKindOfClass:[NSArray class]] && + [(NSArray *)[dictionary objectForKey:@"results"] count] > 0 ) { + self.lastCheckFailed = NO; + + self.latestStoreVersion = [(NSDictionary *)[(NSArray *)[dictionary objectForKey:@"results"] objectAtIndex:0] objectForKey:@"version"]; + self.appStoreURLString = [(NSDictionary *)[(NSArray *)[dictionary objectForKey:@"results"] objectAtIndex:0] objectForKey:@"trackViewUrl"]; + + NSString *ignoredVersion = nil; + if ([self.userDefaults objectForKey:kBITStoreUpdateIgnoreVersion]) { + ignoredVersion = [self.userDefaults objectForKey:kBITStoreUpdateIgnoreVersion]; + BITHockeyLogDebug(@"INFO: Ignored version: %@", ignoredVersion); + } + + if (!self.latestStoreVersion || !self.appStoreURLString) { + return NO; + } else if (ignoredVersion && [ignoredVersion isEqualToString:self.latestStoreVersion]) { + return NO; + } else if (!lastStoreVersion) { + // this is the very first time we get a valid response and + // set the reference of the store result to be equal to the current installed version + // even though the current installed version could be older than the one in the app store + // but this ensures that we never have false alerts, since the version string in + // iTunes Connect doesn't have to match CFBundleVersion or CFBundleShortVersionString + // and even if it matches it is hard/impossible to 100% determine which one it is, + // since they could change at any time + [self.userDefaults setObject:self.currentUUID forKey:kBITStoreUpdateLastUUID]; + [self.userDefaults setObject:self.latestStoreVersion forKey:kBITStoreUpdateLastStoreVersion]; + return NO; + } else { + BITHockeyLogDebug(@"INFO: Compare new version string %@ with %@", self.latestStoreVersion, lastStoreVersion); + + NSComparisonResult comparisonResult = bit_versionCompare(self.latestStoreVersion, lastStoreVersion); + + if (comparisonResult == NSOrderedDescending) { + return YES; + } else { + return NO; + } + + } + } + + return NO; +} + + +#pragma mark - Time + +- (BOOL)shouldAutoCheckForUpdates { + BOOL checkForUpdate = NO; + + switch (self.updateSetting) { + case BITStoreUpdateCheckDaily: { + NSTimeInterval dateDiff = fabs([self.lastCheck timeIntervalSinceNow]); + if (dateDiff != 0) + dateDiff = dateDiff / (60*60*24); + + checkForUpdate = (dateDiff >= 1); + break; + } + case BITStoreUpdateCheckWeekly: { + NSTimeInterval dateDiff = fabs([self.lastCheck timeIntervalSinceNow]); + if (dateDiff != 0) + dateDiff = dateDiff / (60*60*24); + + checkForUpdate = (dateDiff >= 7); + break; + } + case BITStoreUpdateCheckManually: + checkForUpdate = NO; + break; + default: + break; + } + + return checkForUpdate; +} + + +#pragma mark - Private + +- (BOOL)shouldCancelProcessing { + if (self.appEnvironment != BITEnvironmentAppStore) { + BITHockeyLogWarning(@"WARNING: StoreUpdateManager is cancelled because it's not running in an AppStore environment"); + return YES; + } + + if (![self isStoreUpdateManagerEnabled]) { + return YES; + } + + return NO; +} + + +- (BOOL)processStoreResponseWithString:(NSString *)responseString { + if (!responseString) return NO; + + NSData *data = [responseString dataUsingEncoding:NSUTF8StringEncoding]; + + NSError *error = nil; + NSDictionary *json = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + + if (error) { + BITHockeyLogError(@"ERROR: Invalid JSON string. %@", [error localizedDescription]); + return NO; + } + + // remember that we just checked the server + self.lastCheck = [NSDate date]; + + self.updateAvailable = [self hasNewVersion:json]; + + BITHockeyLogDebug(@"INFO: Update available: %i", self.updateAvailable); + + if (self.lastCheckFailed) { + BITHockeyLogError(@"ERROR: Last check failed"); + return NO; + } + + if ([self isUpdateAvailable]) { + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(detectedUpdateFromStoreUpdateManager:newVersion:storeURL:)]) { + [strongDelegate detectedUpdateFromStoreUpdateManager:self newVersion:self.latestStoreVersion storeURL:[NSURL URLWithString:self.appStoreURLString]]; + } + + if (self.updateUIEnabled && BITHockeyBundle()) { + [self showUpdateAlert]; + } else { + // Ignore this version + [self.userDefaults setObject:self.latestStoreVersion forKey:kBITStoreUpdateIgnoreVersion]; + } + } + + return YES; +} + + +#pragma mark - Update Check + +- (void)checkForUpdateManual:(BOOL)manual { + if ([self shouldCancelProcessing]) return; + + if (self.isCheckInProgress) return; + self.checkInProgress = YES; + + // do we need to update? + if (!manual && ![self shouldAutoCheckForUpdates]) { + BITHockeyLogDebug(@"INFO: Update check not needed right now"); + self.checkInProgress = NO; + return; + } + + NSString *country = @""; + if (self.countryCode) { + country = [NSString stringWithFormat:@"&country=%@", self.countryCode]; + } else { + // if the local is by any chance the systemLocale, it could happen that the NSLocaleCountryCode returns nil! + if ([(NSDictionary *)self.currentLocale objectForKey:NSLocaleCountryCode]) { + country = [NSString stringWithFormat:@"&country=%@", [(NSDictionary *)self.currentLocale objectForKey:NSLocaleCountryCode]]; + } else { + // don't check, just to be save + BITHockeyLogError(@"ERROR: Locale returned nil, can't determine the store to use!"); + self.checkInProgress = NO; + return; + } + } + + NSString *appBundleIdentifier = [self.mainBundle objectForInfoDictionaryKey:@"CFBundleIdentifier"]; + + NSString *url = [NSString stringWithFormat:@"https://itunes.apple.com/lookup?bundleId=%@%@", + bit_URLEncodedString(appBundleIdentifier), + country]; + + BITHockeyLogDebug(@"INFO: Sending request to %@", url); + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:(NSURL *)[NSURL URLWithString:url] cachePolicy:1 timeoutInterval:10.0]; + [request setHTTPMethod:@"GET"]; + [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"]; + + __weak typeof (self) weakSelf = self; + NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + __block NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; + + NSURLSessionDataTask *task = [session dataTaskWithRequest:request + completionHandler: ^(NSData *data, NSURLResponse __unused *response, NSError *error) { + typeof (self) strongSelf = weakSelf; + + [session finishTasksAndInvalidate]; + + [strongSelf handleResponeWithData:data error:error]; + }]; + [task resume]; +} + +- (void)handleResponeWithData:(NSData *)responseData error:(NSError *)error{ + self.checkInProgress = NO; + + if (error) { + [self reportError:error]; + } else if ([responseData length]) { + NSString *responseString = [[NSString alloc] initWithBytes:[responseData bytes] length:[responseData length] encoding: NSUTF8StringEncoding]; + BITHockeyLogWarning(@"INFO: Received API response: %@", responseString); + + if (!responseString || ![responseString dataUsingEncoding:NSUTF8StringEncoding]) { + return; + } + + [self processStoreResponseWithString:responseString]; + } +} + +- (void)checkForUpdateDelayed { + [self checkForUpdateManual:NO]; +} + +- (void)checkForUpdate { + [self checkForUpdateManual:YES]; +} + + +// begin the startup process +- (void)startManager { + if ([self shouldCancelProcessing]) return; + + BITHockeyLogDebug(@"INFO: Start UpdateManager"); + + if ([self.userDefaults objectForKey:kBITStoreUpdateDateOfLastCheck]) { + self.lastCheck = [self.userDefaults objectForKey:kBITStoreUpdateDateOfLastCheck]; + } + + if (!self.lastCheck) { + self.lastCheck = [NSDate distantPast]; + } + + [self registerObservers]; + + // we are already delayed, so the notification already came in and this won't invoked twice + switch ([BITHockeyHelper applicationState]) { + case BITApplicationStateActive: + [self didBecomeActiveActions]; + break; + case BITApplicationStateBackground: + case BITApplicationStateInactive: + case BITApplicationStateUnknown: + // do nothing, wait for active state + break; + } +} + + +#pragma mark - Alert + +- (void)showUpdateAlert { + dispatch_async(dispatch_get_main_queue(), ^{ + if (!self.updateAlertShowing) { + NSString *versionString = [NSString stringWithFormat:@"%@ %@", BITHockeyLocalizedString(@"UpdateVersion"), self.latestStoreVersion]; + __weak typeof(self) weakSelf = self; + + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:BITHockeyLocalizedString(@"UpdateAvailable") + message:[NSString stringWithFormat:BITHockeyLocalizedString(@"UpdateAlertTextWithAppVersion"), versionString] + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *ignoreAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"UpdateIgnore") + style:UIAlertActionStyleCancel + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + [strongSelf ignoreAction]; + }]; + [alertController addAction:ignoreAction]; + UIAlertAction *remindAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"UpdateRemindMe") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + [strongSelf remindAction]; + }]; + [alertController addAction:remindAction]; + UIAlertAction *showAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"UpdateShow") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + [strongSelf showAction]; + }]; + [alertController addAction:showAction]; + [self showAlertController:alertController]; + self.updateAlertShowing = YES; + } + }); +} + + +#pragma mark - Properties + +- (void)setLastCheck:(NSDate *)aLastCheck { + if (_lastCheck != aLastCheck) { + _lastCheck = aLastCheck; + + [self.userDefaults setObject:self.lastCheck forKey:kBITStoreUpdateDateOfLastCheck]; + } +} + +- (void)ignoreAction { + self.updateAlertShowing = NO; + [self.userDefaults setObject:self.latestStoreVersion forKey:kBITStoreUpdateIgnoreVersion]; +} + +- (void)remindAction { + self.updateAlertShowing = NO; +} + +- (void)showAction { + self.updateAlertShowing = NO; + [self.userDefaults setObject:self.latestStoreVersion forKey:kBITStoreUpdateIgnoreVersion]; + + if (self.appStoreURLString) { + [[UIApplication sharedApplication] openURL:(NSURL *)[NSURL URLWithString:self.appStoreURLString]]; + } else { + BITHockeyLogWarning(@"WARNING: The app store page couldn't be opened, since we did not get a valid URL from the store API."); + } +} + +@end + +#endif /* HOCKEYSDK_FEATURE_STORE_UPDATES */ diff --git a/submodules/HockeySDK-iOS/Classes/BITStoreUpdateManagerDelegate.h b/submodules/HockeySDK-iOS/Classes/BITStoreUpdateManagerDelegate.h new file mode 100644 index 0000000000..0d629dc0d5 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITStoreUpdateManagerDelegate.h @@ -0,0 +1,59 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2013-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 + +@class BITStoreUpdateManager; + +/** + The `BITStoreUpdateManagerDelegate` formal protocol defines methods for + more interaction with `BITStoreUpdateManager`. + */ + +@protocol BITStoreUpdateManagerDelegate + +@optional + + +///----------------------------------------------------------------------------- +/// @name Update information +///----------------------------------------------------------------------------- + +/** Informs which new version has been reported to be available + + @warning If this is invoked with a simulated new version, the storeURL could be _NIL_ if the current builds + bundle identifier is different to the bundle identifier used in the app store build. + @param storeUpdateManager The `BITStoreUpdateManager` instance invoking this delegate + @param newVersion The new version string reported by the App Store + @param storeURL The App Store URL for this app that could be invoked to let them perform the update. + */ +-(void)detectedUpdateFromStoreUpdateManager:(BITStoreUpdateManager *)storeUpdateManager newVersion:(NSString *)newVersion storeURL:(NSURL *)storeURL; + + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITStoreUpdateManagerPrivate.h b/submodules/HockeySDK-iOS/Classes/BITStoreUpdateManagerPrivate.h new file mode 100644 index 0000000000..156913c514 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITStoreUpdateManagerPrivate.h @@ -0,0 +1,71 @@ +/* + * Author: Andreas Linde + * Peter Steinberger + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde. + * 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. + */ + + +#if HOCKEYSDK_FEATURE_STORE_UPDATES + +@interface BITStoreUpdateManager () { +} + +///----------------------------------------------------------------------------- +/// @name Delegate +///----------------------------------------------------------------------------- + +/** + Sets the optional `BITStoreUpdateManagerDelegate` delegate. + */ +@property (nonatomic, weak) id delegate; + + +// is an update available? +@property (nonatomic, assign, getter=isUpdateAvailable) BOOL updateAvailable; + +// are we currently checking for updates? +@property (nonatomic, assign, getter=isCheckInProgress) BOOL checkInProgress; + +@property (nonatomic, strong) NSDate *lastCheck; + +// used by BITHockeyManager if disable status is changed +@property (nonatomic, getter = isStoreUpdateManagerEnabled) BOOL enableStoreUpdateManager; + +#pragma mark - For Testing + +@property (nonatomic, strong) NSBundle *mainBundle; +@property (nonatomic, strong) NSLocale *currentLocale; +@property (nonatomic, strong) NSUserDefaults *userDefaults; + +- (BOOL)shouldAutoCheckForUpdates; +- (BOOL)hasNewVersion:(NSDictionary *)dictionary; +- (BOOL)processStoreResponseWithString:(NSString *)responseString; +- (void)checkForUpdateDelayed; + +@end + +#endif /* HOCKEYSDK_FEATURE_STORE_UPDATES */ diff --git a/submodules/HockeySDK-iOS/Classes/BITTelemetryContext.h b/submodules/HockeySDK-iOS/Classes/BITTelemetryContext.h new file mode 100644 index 0000000000..31ef5ba742 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITTelemetryContext.h @@ -0,0 +1,106 @@ +#import "HockeySDKFeatureConfig.h" + +#if HOCKEYSDK_FEATURE_METRICS + +#import +#import "BITApplication.h" +#import "BITDevice.h" +#import "BITInternal.h" +#import "BITUser.h" +#import "BITSession.h" + +@class BITPersistence; + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +/** + * Context object which contains information about the device, user, session etc. + */ +@interface BITTelemetryContext : NSObject + +///----------------------------------------------------------------------------- +/// @name Initialisation +///----------------------------------------------------------------------------- + +/** + * The persistence instance used to save/load metadata. + */ +@property(nonatomic, strong) BITPersistence *persistence; + +/** + * The instrumentation key of the app. + */ +@property(nonatomic, copy) NSString *appIdentifier; + +/** + * A queue which makes array operations thread safe. + */ +@property (nonatomic, strong) dispatch_queue_t operationsQueue; + +/** + * The application context. + */ +@property(nonatomic, strong, readonly) BITApplication *application; + +/** + * The device context. + */ +@property (nonatomic, strong, readonly)BITDevice *device; + +/** + * The session context. + */ +@property (nonatomic, strong, readonly)BITSession *session; + +/** + * The user context. + */ +@property (nonatomic, strong, readonly)BITUser *user; + +/** + * The internal context. + */ +@property (nonatomic, strong, readonly)BITInternal *internal; + +/** + * Initializes a telemetry context. + * + * @param appIdentifier the appIdentifier of the app + * @param persistence the persistence used to save and load metadata + * + * @return the telemetry context + */ +- (instancetype)initWithAppIdentifier:(NSString *)appIdentifier persistence:(BITPersistence *)persistence; + +///----------------------------------------------------------------------------- +/// @name Helper +///----------------------------------------------------------------------------- + +/** + * A dictionary which holds static tag fields for the purpose of caching + */ +@property (nonatomic, strong) NSDictionary *tags; + +/** + * Returns context objects as dictionary. + * + * @return a dictionary containing all context fields + */ +- (NSDictionary *)contextDictionary; + +///----------------------------------------------------------------------------- +/// @name Getter/Setter +///----------------------------------------------------------------------------- + +- (void)setSessionId:(NSString *)sessionId; + +- (void)setIsFirstSession:(NSString *)isFirstSession; + +- (void)setIsNewSession:(NSString *)isNewSession; + +@end + +NS_ASSUME_NONNULL_END + +#endif /* HOCKEYSDK_FEATURE_METRICS */ diff --git a/submodules/HockeySDK-iOS/Classes/BITTelemetryContext.m b/submodules/HockeySDK-iOS/Classes/BITTelemetryContext.m new file mode 100644 index 0000000000..b0119268a5 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITTelemetryContext.m @@ -0,0 +1,380 @@ +#import +#import "BITTelemetryContext.h" + +#if HOCKEYSDK_FEATURE_METRICS + +#import "BITMetricsManagerPrivate.h" +#import "BITHockeyHelper.h" +#import "BITPersistence.h" +#import "BITPersistencePrivate.h" + +static NSString *const kBITUserMetaData = @"BITUserMetaData"; + +static char *const BITContextOperationsQueue = "net.hockeyapp.telemetryContextQueue"; + +@implementation BITTelemetryContext + +@synthesize appIdentifier = _appIdentifier; +@synthesize persistence = _persistence; + +#pragma mark - Initialisation + +-(instancetype)init { + + if((self = [super init])) { + _operationsQueue = dispatch_queue_create(BITContextOperationsQueue, DISPATCH_QUEUE_CONCURRENT); + } + return self; +} + +- (instancetype)initWithAppIdentifier:(NSString *)appIdentifier persistence:(BITPersistence *)persistence { + + if ((self = [self init])) { + _persistence = persistence; + _appIdentifier = appIdentifier; + BITDevice *deviceContext = [BITDevice new]; + deviceContext.model = bit_devicePlatform(); + deviceContext.type = bit_deviceType(); + deviceContext.osVersion = bit_osVersionBuild(); + deviceContext.os = bit_osName(); + deviceContext.deviceId = bit_appAnonID(NO); + deviceContext.locale = bit_deviceLocale(); + deviceContext.language = bit_deviceLanguage(); + deviceContext.screenResolution = bit_screenSize(); + deviceContext.oemName = @"Apple"; + + BITInternal *internalContext = [BITInternal new]; + internalContext.sdkVersion = bit_sdkVersion(); + + BITApplication *applicationContext = [BITApplication new]; + applicationContext.version = bit_appVersion(); + + BITUser *userContext = [self loadUser]; + if (!userContext) { + userContext = [self newUser]; + [self saveUser:userContext]; + } + + BITSession *sessionContext = [BITSession new]; + + _application = applicationContext; + _device = deviceContext; + _user = userContext; + _internal = internalContext; + _session = sessionContext; + _tags = [self tags]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - User + +- (BITUser *)newUser { + return ({ + BITUser *user = [BITUser new]; + user.userId = bit_appAnonID(NO); + user; + }); +} + +- (void)saveUser:(BITUser *)user{ + NSDictionary *userMetaData = @{kBITUserMetaData : user}; + [self.persistence persistMetaData:userMetaData]; +} + +- (nullable BITUser *)loadUser{ + NSDictionary *metaData =[self.persistence metaData]; + BITUser *user = [metaData objectForKey:kBITUserMetaData]; + return user; +} + +#pragma mark - Network + +#pragma mark - Getter/Setter properties + +- (NSString *)appIdentifier { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self->_appIdentifier; + }); + return tmp; +} + +- (void)setAppIdentifier:(NSString *)appIdentifier { + NSString* tmp = [appIdentifier copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self->_appIdentifier = tmp; + }); +} + +- (NSString *)screenResolution { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.device.screenResolution; + }); + return tmp; +} + +- (void)setScreenResolution:(NSString *)screenResolution { + NSString* tmp = [screenResolution copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.device.screenResolution = tmp; + }); +} + +- (NSString *)appVersion { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.application.version; + }); + return tmp; +} + +- (void)setAppVersion:(NSString *)appVersion { + NSString* tmp = [appVersion copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.application.version = tmp; + }); +} + +- (NSString *)anonymousUserId { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.user.userId; + }); + return tmp; +} + +- (void)setAnonymousUserId:(NSString *)userId { + NSString* tmp = [userId copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.user.userId = tmp; + }); +} + +- (NSString *)anonymousUserAquisitionDate { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.user.anonUserAcquisitionDate; + }); + return tmp; +} + +- (void)setAnonymousUserAquisitionDate:(NSString *)anonymousUserAquisitionDate { + NSString* tmp = [anonymousUserAquisitionDate copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.user.anonUserAcquisitionDate = tmp; + }); +} + +- (NSString *)sdkVersion { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.internal.sdkVersion; + }); + return tmp; +} + +- (void)setSdkVersion:(NSString *)sdkVersion { + NSString* tmp = [sdkVersion copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.internal.sdkVersion = tmp; + }); +} + +- (NSString *)sessionId { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.session.sessionId; + }); + return tmp; +} + +- (void)setSessionId:(NSString *)sessionId { + NSString* tmp = [sessionId copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.session.sessionId = tmp; + }); +} + +- (NSString *)isFirstSession { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.session.isFirst; + }); + return tmp; +} + +- (void)setIsFirstSession:(NSString *)isFirstSession { + NSString* tmp = [isFirstSession copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.session.isFirst = tmp; + }); +} + +- (NSString *)isNewSession { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.session.isNew; + }); + return tmp; +} + +- (void)setIsNewSession:(NSString *)isNewSession { + NSString* tmp = [isNewSession copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.session.isNew = tmp; + }); +} + +- (NSString *)osVersion { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.device.osVersion; + }); + return tmp; +} + +- (void)setOsVersion:(NSString *)osVersion { + NSString* tmp = [osVersion copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.device.osVersion = tmp; + }); +} + +- (NSString *)osName { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.device.os; + }); + return tmp; +} + +- (void)setOsName:(NSString *)osName { + NSString* tmp = [osName copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.device.os = tmp; + }); +} + +- (NSString *)deviceModel { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.device.model; + }); + return tmp; +} + +- (void)setDeviceModel:(NSString *)deviceModel { + NSString* tmp = [deviceModel copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.device.model = tmp; + }); +} + +- (NSString *)deviceOemName { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.device.oemName; + }); + return tmp; +} + +- (void)setDeviceOemName:(NSString *)oemName { + NSString* tmp = [oemName copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.device.oemName = tmp; + }); +} + +- (NSString *)osLocale { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.device.locale; + }); + return tmp; +} + +- (void)setOsLocale:(NSString *)osLocale { + NSString* tmp = [osLocale copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.device.locale = tmp; + }); +} + +- (NSString *)osLanguage { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.device.language; + }); + return tmp; +} + +- (void)setOsLanguage:(NSString *)osLanguage { + NSString* tmp = [osLanguage copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.device.language = tmp; + }); +} + +- (NSString *)deviceId { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.device.deviceId; + }); + return tmp; +} + +- (void)setDeviceId:(NSString *)deviceId { + NSString* tmp = [deviceId copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.device.deviceId = tmp; + }); +} + +- (NSString *)deviceType { + __block NSString *tmp; + dispatch_sync(self.operationsQueue, ^{ + tmp = self.device.type; + }); + return tmp; +} + +- (void)setDeviceType:(NSString *)deviceType { + NSString* tmp = [deviceType copy]; + dispatch_barrier_async(self.operationsQueue, ^{ + self.device.type = tmp; + }); +} + +#pragma mark - Custom getter +#pragma mark - Helper + +- (NSDictionary *)contextDictionary { + __block NSMutableDictionary *tmp = [NSMutableDictionary new]; + dispatch_sync(self.operationsQueue, ^{ + [tmp addEntriesFromDictionary:self.tags]; + [tmp addEntriesFromDictionary:[self.session serializeToDictionary]]; + [tmp addEntriesFromDictionary:[self.user serializeToDictionary]]; + }); + return tmp; +} + +- (NSDictionary *)tags { + if(!_tags){ + NSMutableDictionary *tags = [self.application serializeToDictionary].mutableCopy; + [tags addEntriesFromDictionary:[self.application serializeToDictionary]]; + [tags addEntriesFromDictionary:[self.internal serializeToDictionary]]; + [tags addEntriesFromDictionary:[self.device serializeToDictionary]]; + _tags = tags; + } + return _tags; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_METRICS */ diff --git a/submodules/HockeySDK-iOS/Classes/BITTelemetryData.h b/submodules/HockeySDK-iOS/Classes/BITTelemetryData.h new file mode 100644 index 0000000000..50539a1074 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITTelemetryData.h @@ -0,0 +1,18 @@ +#import "BITTelemetryObject.h" + +#import "HockeySDKNullability.h" +NS_ASSUME_NONNULL_BEGIN + +///Data contract class for type BITTelemetryData. +@interface BITTelemetryData : BITTelemetryObject + +@property (nonatomic, readonly, copy) NSString *envelopeTypeName; +@property (nonatomic, readonly, copy) NSString *dataTypeName; + +@property (nonatomic, copy) NSNumber *version; +@property (nonatomic, copy) NSString *name; +@property (nonatomic, strong) NSDictionary *properties; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/HockeySDK-iOS/Classes/BITTelemetryData.m b/submodules/HockeySDK-iOS/Classes/BITTelemetryData.m new file mode 100644 index 0000000000..285ff321f5 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITTelemetryData.m @@ -0,0 +1,33 @@ +#import "BITTelemetryData.h" + +@implementation BITTelemetryData + +- (NSDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary].mutableCopy; + if (self.version != nil) { + [dict setObject:self.version forKey:@"ver"]; + } + + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if(self) { + _version = (NSNumber *)[coder decodeObjectForKey:@"self.version"]; + _name = (NSString *)[coder decodeObjectForKey:@"self.name"]; + _properties = (NSDictionary *)[coder decodeObjectForKey:@"self.properties"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.version forKey:@"self.version"]; + [coder encodeObject:self.name forKey:@"self.name"]; + [coder encodeObject:self.properties forKey:@"self.properties"]; +} + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITTelemetryObject.h b/submodules/HockeySDK-iOS/Classes/BITTelemetryObject.h new file mode 100644 index 0000000000..09b1dda154 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITTelemetryObject.h @@ -0,0 +1,8 @@ +#import + + +@interface BITTelemetryObject : NSObject + +- (NSDictionary *)serializeToDictionary; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITTelemetryObject.m b/submodules/HockeySDK-iOS/Classes/BITTelemetryObject.m new file mode 100644 index 0000000000..c996738696 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITTelemetryObject.m @@ -0,0 +1,18 @@ +#import "BITTelemetryObject.h" + +@implementation BITTelemetryObject + +// empty implementation for the base class +- (NSDictionary *)serializeToDictionary{ + return [NSDictionary dictionary]; +} + +- (void)encodeWithCoder:(NSCoder *) __unused coder { +} + +- (instancetype)initWithCoder:(NSCoder *) __unused coder { + return [super init]; +} + + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITUpdateManager.h b/submodules/HockeySDK-iOS/Classes/BITUpdateManager.h new file mode 100644 index 0000000000..c39cf714ff --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITUpdateManager.h @@ -0,0 +1,238 @@ +/* + * Author: Andreas Linde + * Peter Steinberger + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde. + * 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 "BITHockeyBaseManager.h" + + +/** + * Update check interval + */ +typedef NS_ENUM (NSUInteger, BITUpdateSetting) { + /** + * On every startup or or when the app comes to the foreground + */ + BITUpdateCheckStartup = 0, + /** + * Once a day + */ + BITUpdateCheckDaily = 1, + /** + * Manually + */ + BITUpdateCheckManually = 2 +}; + +@protocol BITUpdateManagerDelegate; + +@class BITAppVersionMetaInfo; +@class BITUpdateViewController; + +/** + The update manager module. + + This is the HockeySDK module for handling app updates when using Ad-Hoc or Enterprise provisioning profiles. + This module handles version updates, presents update and version information in an App Store like user interface, + collects usage information and provides additional authorization options when using Ad-Hoc provisioning profiles. + + By default, this module automatically disables itself when running in an App Store build! + + The protocol `BITUpdateManagerDelegate` provides delegates to inform about events and adjust a few behaviors. + + To use the server side restriction feature, to provide updates only to specific users, you need to setup the + `BITAuthenticator` class. This allows the update request to tell the server which user is using the app on the + current device and then let the server decide which updates the device may see. + + */ + +@interface BITUpdateManager : BITHockeyBaseManager + +///----------------------------------------------------------------------------- +/// @name Update Checking +///----------------------------------------------------------------------------- + +// see HockeyUpdateSetting-enum. Will be saved in user defaults. +// default value: HockeyUpdateCheckStartup +/** + When to check for new updates. + + Defines when the SDK should check if there is a new update available on the + server. This must be assigned one of the following, see `BITUpdateSetting`: + + - `BITUpdateCheckStartup`: On every startup or or when the app comes to the foreground + - `BITUpdateCheckDaily`: Once a day + - `BITUpdateCheckManually`: Manually + + When running the app from the App Store, this setting is ignored. + + **Default**: BITUpdateCheckStartup + + @warning When setting this to `BITUpdateCheckManually` you need to either + invoke the update checking process yourself with `checkForUpdate` somehow, e.g. by + proving an update check button for the user or integrating the Update View into your + user interface. + @see BITUpdateSetting + @see checkForUpdateOnLaunch + @see checkForUpdate + */ +@property (nonatomic, assign) BITUpdateSetting updateSetting; + + +/** + Flag that determines whether the automatic update checks should be done. + + If this is enabled the update checks will be performed automatically depending on the + `updateSetting` property. If this is disabled the `updateSetting` property will have + no effect, and checking for updates is totally up to be done by yourself. + + When running the app from the App Store, this setting is ignored. + + *Default*: _YES_ + + @warning When setting this to `NO` you need to invoke update checks yourself! + @see updateSetting + @see checkForUpdate + */ +@property (nonatomic, assign, getter=isCheckForUpdateOnLaunch) BOOL checkForUpdateOnLaunch; + + +// manually start an update check +/** + Check for an update + + Call this to trigger a check if there is a new update available on the HockeyApp servers. + + When running the app from the App Store, this method call is ignored. + + @see updateSetting + @see checkForUpdateOnLaunch + */ +- (void)checkForUpdate; + + +///----------------------------------------------------------------------------- +/// @name Update Notification +///----------------------------------------------------------------------------- + +/** + Flag that determines if update alerts should be repeatedly shown + + If enabled the update alert shows on every startup and whenever the app becomes active, + until the update is installed. + If disabled the update alert is only shown once ever and it is up to you to provide an + alternate way for the user to navigate to the update UI or update in another way. + + When running the app from the App Store, this setting is ignored. + + *Default*: _YES_ + */ +@property (nonatomic, assign) BOOL alwaysShowUpdateReminder; + + +/** + Flag that determines if the update alert should show a direct install option + + If enabled the update alert shows an additional option which allows to invoke the update + installation process directly instead of viewing the update UI first. + By default the alert only shows a `Show` and `Ignore` option. + + When running the app from the App Store, this setting is ignored. + + *Default*: _NO_ + */ +@property (nonatomic, assign, getter=isShowingDirectInstallOption) BOOL showDirectInstallOption; + + +///----------------------------------------------------------------------------- +/// @name Expiry +///----------------------------------------------------------------------------- + +/** + Expiry date of the current app version + + If set, the app will get unusable at the given date by presenting a blocking view on + top of the apps UI so that no interaction is possible. To present a custom UI, check + the documentation of the + `[BITUpdateManagerDelegate shouldDisplayExpiryAlertForUpdateManager:]` delegate. + + Once the expiry date is reached, the app will no longer check for updates or + send any usage data to the server! + + When running the app from the App Store, this setting is ignored. + + *Default*: nil + @see disableUpdateCheckOptionWhenExpired + @see [BITUpdateManagerDelegate shouldDisplayExpiryAlertForUpdateManager:] + @see [BITUpdateManagerDelegate didDisplayExpiryAlertForUpdateManager:] + @warning This only works when using Ad-Hoc provisioning profiles! + */ +@property (nonatomic, strong) NSDate *expiryDate; + +/** + Disable the update check button from expiry screen or alerts + + If do not want your users to be able to check for updates once a version is expired, + then enable this property. + + If this is not enabled, the users will be able to check for updates and install them + if any is available for the current device. + + *Default*: NO + @see expiryDate + @see [BITUpdateManagerDelegate shouldDisplayExpiryAlertForUpdateManager:] + @see [BITUpdateManagerDelegate didDisplayExpiryAlertForUpdateManager:] + @warning This only works when using Ad-Hoc provisioning profiles! +*/ +@property (nonatomic) BOOL disableUpdateCheckOptionWhenExpired; + + +///----------------------------------------------------------------------------- +/// @name User Interface +///----------------------------------------------------------------------------- + + +/** + Present the modal update user interface. + + @warning Make sure to call this method from the main thread! + */ +- (void)showUpdateView; + + +/** + Create an update view + + @param modal Return a view which is ready for modal presentation with an integrated navigation bar + @return BITUpdateViewController The update user interface view controller, + e.g. to push it onto a navigation stack. + */ +- (BITUpdateViewController *)hockeyViewController:(BOOL)modal; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITUpdateManager.m b/submodules/HockeySDK-iOS/Classes/BITUpdateManager.m new file mode 100644 index 0000000000..7dcb7b7645 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITUpdateManager.m @@ -0,0 +1,1152 @@ +/* + * Author: Andreas Linde + * Peter Steinberger + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde. + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_UPDATES + +#import + +#import "HockeySDKPrivate.h" +#import "BITHockeyHelper.h" +#import "BITHockeyHelper+Application.h" + +#import "BITHockeyBaseManagerPrivate.h" +#import "BITUpdateManagerPrivate.h" +#import "BITUpdateViewControllerPrivate.h" +#import "BITAppVersionMetaInfo.h" + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER +#import "BITCrashManagerPrivate.h" +#endif + +typedef NS_ENUM(NSInteger, BITUpdateAlertViewTag) { + BITUpdateAlertViewTagDefaultUpdate = 0, + BITUpdateAlertViewTagNeverEndingAlertView = 1, + BITUpdateAlertViewTagMandatoryUpdate = 2, +}; + +@interface BITUpdateManager () + +@property (nonatomic, copy) NSString *currentAppVersion; +@property (nonatomic) BOOL dataFound; +@property (nonatomic) BOOL showFeedback; +@property (nonatomic) BOOL updateAlertShowing; +@property (nonatomic) BOOL lastCheckFailed; +@property (nonatomic, strong) NSFileManager *fileManager; +@property (nonatomic, copy) NSString *updateDir; +@property (nonatomic, copy) NSString *usageDataFile; +@property (nonatomic, weak) id appDidBecomeActiveObserver; +@property (nonatomic, weak) id appDidEnterBackgroundObserver; +@property (nonatomic, weak) id networkDidBecomeReachableObserver; +@property (nonatomic) BOOL didStartUpdateProcess; +@property (nonatomic) BOOL didEnterBackgroundState; +@property (nonatomic) BOOL firstStartAfterInstall; +@property (nonatomic, strong) NSNumber *versionID; +@property (nonatomic, copy) NSString *versionUUID; +@property (nonatomic, copy) NSString *uuid; +@property (nonatomic, copy) NSString *blockingScreenMessage; +@property (nonatomic, strong) NSDate *lastUpdateCheckFromBlockingScreen; + +@end + +@implementation BITUpdateManager + + +#pragma mark - private + +- (void)reportError:(NSError *)error { + BITHockeyLogError(@"ERROR: %@", [error localizedDescription]); + self.lastCheckFailed = YES; + + // only show error if we enable that + if (self.showFeedback) { + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:BITHockeyLocalizedString(@"UpdateError") + message:[error localizedDescription] + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *okAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyOK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) {}]; + [alertController addAction:okAction]; + [self showAlertController:alertController]; + self.showFeedback = NO; + } +} + + +- (void)didBecomeActiveActions { + if ([self isUpdateManagerDisabled]) return; + + // this is a special iOS 8 case for handling the case that the app is not moved to background + // once the users accepts the iOS install alert button. Without this, the install process doesn't start. + // + // Important: The iOS dialog offers the user to deny installation, we can't find out which button + // was tapped, so we assume the user agreed + if (self.didStartUpdateProcess) { + self.didStartUpdateProcess = NO; + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(updateManagerWillExitApp:)]) { + [strongDelegate updateManagerWillExitApp:self]; + } + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER + [[BITHockeyManager sharedHockeyManager].crashManager leavingAppSafely]; +#endif + + // for now we simply exit the app, later SDK versions might optionally show an alert with localized text + // describing the user to press the home button to start the update process + exit(0); + } + + if (!self.didEnterBackgroundState) return; + + self.didEnterBackgroundState = NO; + + [self checkExpiryDateReached]; + if ([self expiryDateReached]) return; + + [self startUsage]; + + if ([self isCheckForUpdateOnLaunch] && [self shouldCheckForUpdates]) { + [self checkForUpdate]; + } +} + +- (void)didEnterBackgroundActions { + self.didEnterBackgroundState = NO; + + if ([BITHockeyHelper applicationState] == BITApplicationStateBackground) { + self.didEnterBackgroundState = YES; + } +} + + +#pragma mark - Observers +- (void) registerObservers { + __weak typeof(self) weakSelf = self; + if(nil == self.appDidEnterBackgroundObserver) { + self.appDidEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf didEnterBackgroundActions]; + }]; + } + if(nil == self.appDidBecomeActiveObserver) { + self.appDidBecomeActiveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf didBecomeActiveActions]; + }]; + } + if(nil == self.networkDidBecomeReachableObserver) { + self.networkDidBecomeReachableObserver = [[NSNotificationCenter defaultCenter] addObserverForName:BITHockeyNetworkDidBecomeReachableNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification __unused *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf didBecomeActiveActions]; + }]; + } +} + +- (void) unregisterObservers { + id strongDidEnterBackgroundObserver = self.appDidEnterBackgroundObserver; + id strongDidBecomeActiveObserver = self.appDidBecomeActiveObserver; + id strongNetworkDidBecomeReachableObserver = self.networkDidBecomeReachableObserver; + if(strongDidEnterBackgroundObserver) { + [[NSNotificationCenter defaultCenter] removeObserver:strongDidEnterBackgroundObserver]; + self.appDidEnterBackgroundObserver = nil; + } + if(strongDidBecomeActiveObserver) { + [[NSNotificationCenter defaultCenter] removeObserver:strongDidBecomeActiveObserver]; + self.appDidBecomeActiveObserver = nil; + } + if(strongNetworkDidBecomeReachableObserver) { + [[NSNotificationCenter defaultCenter] removeObserver:strongNetworkDidBecomeReachableObserver]; + self.networkDidBecomeReachableObserver = nil; + } +} + + +#pragma mark - Expiry + +- (BOOL)expiryDateReached { + if (self.appEnvironment != BITEnvironmentOther) return NO; + + if (self.expiryDate) { + NSDate *currentDate = [NSDate date]; + if ([currentDate compare:self.expiryDate] != NSOrderedAscending) + return YES; + } + + return NO; +} + +- (void)checkExpiryDateReached { + if (![self expiryDateReached]) return; + + BOOL shouldShowDefaultAlert = YES; + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(shouldDisplayExpiryAlertForUpdateManager:)]) { + shouldShowDefaultAlert = [strongDelegate shouldDisplayExpiryAlertForUpdateManager:self]; + } + + if (shouldShowDefaultAlert) { + NSString *appName = bit_appName(BITHockeyLocalizedString(@"HockeyAppNamePlaceholder")); + if (!self.blockingScreenMessage) + self.blockingScreenMessage = [NSString stringWithFormat:BITHockeyLocalizedString(@"UpdateExpired"), appName]; + [self showBlockingScreen:self.blockingScreenMessage image:@"authorize_denied.png"]; + + if ([strongDelegate respondsToSelector:@selector(didDisplayExpiryAlertForUpdateManager:)]) { + [strongDelegate didDisplayExpiryAlertForUpdateManager:self]; + } + + // the UI is now blocked, make sure we don't add our UI on top of it over and over again + [self unregisterObservers]; + } +} + +#pragma mark - Usage + +- (void)loadAppVersionUsageData { + self.currentAppVersionUsageTime = @0; + + if ([self expiryDateReached]) return; + + BOOL newVersion = NO; + + if (![[NSUserDefaults standardUserDefaults] valueForKey:kBITUpdateUsageTimeForUUID]) { + newVersion = YES; + } else { + if ([(NSString *)[[NSUserDefaults standardUserDefaults] valueForKey:kBITUpdateUsageTimeForUUID] compare:self.uuid] != NSOrderedSame) { + newVersion = YES; + } + } + + if (newVersion) { + [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithDouble:[[NSDate date] timeIntervalSinceReferenceDate]] forKey:kBITUpdateDateOfVersionInstallation]; + [[NSUserDefaults standardUserDefaults] setObject:self.uuid forKey:kBITUpdateUsageTimeForUUID]; + [self storeUsageTimeForCurrentVersion:[NSNumber numberWithDouble:0]]; + } else { + if (![self.fileManager fileExistsAtPath:self.usageDataFile]) + return; + + NSData *codedData = [[NSData alloc] initWithContentsOfFile:self.usageDataFile]; + if (codedData == nil) return; + + NSKeyedUnarchiver *unarchiver = nil; + + @try { + unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:codedData]; + } + @catch (NSException __unused *exception) { + return; + } + + if ([unarchiver containsValueForKey:kBITUpdateUsageTimeOfCurrentVersion]) { + self.currentAppVersionUsageTime = [unarchiver decodeObjectForKey:kBITUpdateUsageTimeOfCurrentVersion]; + } + + [unarchiver finishDecoding]; + } +} + +- (void)startUsage { + if ([self expiryDateReached]) return; + + self.usageStartTimestamp = [NSDate date]; +} + +- (void)stopUsage { + if (self.appEnvironment != BITEnvironmentOther) return; + if ([self expiryDateReached]) return; + + double timeDifference = [[NSDate date] timeIntervalSinceReferenceDate] - [self.usageStartTimestamp timeIntervalSinceReferenceDate]; + double previousTimeDifference = [self.currentAppVersionUsageTime doubleValue]; + + [self storeUsageTimeForCurrentVersion:[NSNumber numberWithDouble:previousTimeDifference + timeDifference]]; +} + +- (void) storeUsageTimeForCurrentVersion:(NSNumber *)usageTime { + if (self.appEnvironment != BITEnvironmentOther) return; + + NSMutableData *data = [[NSMutableData alloc] init]; + NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; + + [archiver encodeObject:usageTime forKey:kBITUpdateUsageTimeOfCurrentVersion]; + + [archiver finishEncoding]; + [data writeToFile:self.usageDataFile atomically:YES]; + + self.currentAppVersionUsageTime = usageTime; +} + +- (NSString *)currentUsageString { + double currentUsageTime = [self.currentAppVersionUsageTime doubleValue]; + + if (currentUsageTime > 0) { + // round (up) to 1 minute + return [NSString stringWithFormat:@"%.0f", ceil(currentUsageTime / 60.0)*60]; + } else { + return @"0"; + } +} + +- (NSString *)installationDateString { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + [formatter setDateFormat:@"MM/dd/yyyy"]; + double installationTimeStamp = [[NSUserDefaults standardUserDefaults] doubleForKey:kBITUpdateDateOfVersionInstallation]; + if (installationTimeStamp == 0.0) { + return [formatter stringFromDate:[NSDate date]]; + } else { + return [formatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:installationTimeStamp]]; + } +} + + +#pragma mark - Cache + +- (void)checkUpdateAvailable { + // check if there is an update available + NSComparisonResult comparisonResult = bit_versionCompare(self.newestAppVersion.version, self.currentAppVersion); + + if (comparisonResult == NSOrderedDescending) { + self.updateAvailable = YES; + } else if (comparisonResult == NSOrderedSame) { + // compare using the binary UUID and stored version id + self.updateAvailable = NO; + if (self.firstStartAfterInstall) { + if ([self.newestAppVersion hasUUID:self.uuid]) { + self.versionUUID = [self.uuid copy]; + self.versionID = [self.newestAppVersion.versionID copy]; + [self saveAppCache]; + } else { + [self.appVersions enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + if (idx > 0 && [obj isKindOfClass:[BITAppVersionMetaInfo class]]) { + NSComparisonResult compareVersions = bit_versionCompare([(BITAppVersionMetaInfo *)obj version], self.currentAppVersion); + BOOL uuidFound = [(BITAppVersionMetaInfo *)obj hasUUID:self.uuid]; + + if (uuidFound) { + self.versionUUID = [self.uuid copy]; + self.versionID = [[(BITAppVersionMetaInfo *)obj versionID] copy]; + [self saveAppCache]; + + self.updateAvailable = YES; + } + + if (compareVersions != NSOrderedSame || uuidFound) { + *stop = YES; + } + } + }]; + } + } else { + if ([self.newestAppVersion.versionID compare:self.versionID] == NSOrderedDescending) + self.updateAvailable = YES; + } + } +} + +- (void)loadAppCache { + self.firstStartAfterInstall = NO; + self.versionUUID = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateInstalledUUID]; + if (!self.versionUUID) { + self.firstStartAfterInstall = YES; + } else { + if ([self.uuid compare:self.versionUUID] != NSOrderedSame) + self.firstStartAfterInstall = YES; + } + self.versionID = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateInstalledVersionID]; + self.companyName = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateCurrentCompanyName]; + + NSData *savedHockeyData = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateArrayOfLastCheck]; + NSArray *savedHockeyCheck = nil; + if (savedHockeyData) { + savedHockeyCheck = [NSKeyedUnarchiver unarchiveObjectWithData:savedHockeyData]; + } + if (savedHockeyCheck) { + self.appVersions = [NSArray arrayWithArray:savedHockeyCheck]; + [self checkUpdateAvailable]; + } else { + self.appVersions = nil; + } +} + +- (void)saveAppCache { + if (self.companyName) { + [[NSUserDefaults standardUserDefaults] setObject:self.companyName forKey:kBITUpdateCurrentCompanyName]; + } + if (self.versionUUID) { + [[NSUserDefaults standardUserDefaults] setObject:self.versionUUID forKey:kBITUpdateInstalledUUID]; + } + if (self.versionID) { + [[NSUserDefaults standardUserDefaults] setObject:self.versionID forKey:kBITUpdateInstalledVersionID]; + } + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.appVersions]; + [[NSUserDefaults standardUserDefaults] setObject:data forKey:kBITUpdateArrayOfLastCheck]; +} + + +#pragma mark - Init + +- (instancetype)init { + if ((self = [super init])) { + _delegate = nil; + _expiryDate = nil; + _checkInProgress = NO; + _dataFound = NO; + _updateAvailable = NO; + _lastCheckFailed = NO; + _currentAppVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; + _blockingView = nil; + _lastCheck = nil; + _uuid = [[self executableUUID] copy]; + _versionUUID = nil; + _versionID = nil; + _sendUsageData = YES; + _disableUpdateManager = NO; + _firstStartAfterInstall = NO; + _companyName = nil; + _currentAppVersionUsageTime = @0; + + // set defaults + _showDirectInstallOption = NO; + _alwaysShowUpdateReminder = YES; + _checkForUpdateOnLaunch = YES; + _updateSetting = BITUpdateCheckStartup; + + if ([[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateDateOfLastCheck]) { + // we did write something else in the past, so for compatibility reasons do this + id tempLastCheck = [[NSUserDefaults standardUserDefaults] objectForKey:kBITUpdateDateOfLastCheck]; + if ([tempLastCheck isKindOfClass:[NSDate class]]) { + _lastCheck = tempLastCheck; + } + } + + if (!_lastCheck) { + _lastCheck = [NSDate distantPast]; + } + + if (!BITHockeyBundle()) { + BITHockeyLogWarning(@"[HockeySDK] WARNING: %@ is missing, make sure it is added!", BITHOCKEYSDK_BUNDLE); + } + + _fileManager = [[NSFileManager alloc] init]; + + _usageDataFile = [bit_settingsDir() stringByAppendingPathComponent:BITHOCKEY_USAGE_DATA]; + + [self loadAppCache]; + + _installationIdentification = [self stringValueFromKeychainForKey:kBITUpdateInstallationIdentification]; + + [self loadAppVersionUsageData]; + [self startUsage]; + + NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter]; + [dnc addObserver:self selector:@selector(stopUsage) name:UIApplicationWillTerminateNotification object:nil]; + [dnc addObserver:self selector:@selector(stopUsage) name:UIApplicationWillResignActiveNotification object:nil]; + } + return self; +} + +- (void)dealloc { + [self unregisterObservers]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillTerminateNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; +} + + +#pragma mark - BetaUpdateUI + +- (BITUpdateViewController *)hockeyViewController:(BOOL)modal { + if (self.appEnvironment != BITEnvironmentOther) { + BITHockeyLogWarning(@"[HockeySDK] This should not be called from an app store build!"); + // return an empty view controller instead + BITHockeyBaseViewController *blankViewController = [[BITHockeyBaseViewController alloc] initWithModalStyle:modal]; + return (BITUpdateViewController *)blankViewController; + } + return [[BITUpdateViewController alloc] initWithModalStyle:modal]; +} + +- (void)showUpdateView { + if (self.appEnvironment != BITEnvironmentOther) { + BITHockeyLogWarning(@"[HockeySDK] This should not be called from an app store build!"); + return; + } + + if (self.currentHockeyViewController) { + BITHockeyLogDebug(@"INFO: Update view already visible, aborting"); + return; + } + + BITUpdateViewController *updateViewController = [self hockeyViewController:YES]; + if ([self hasNewerMandatoryVersion] || [self expiryDateReached]) { + [updateViewController setMandatoryUpdate: YES]; + } + dispatch_async(dispatch_get_main_queue(), ^{ + [self showView:updateViewController]; + }); +} + + +- (void)showCheckForUpdateAlert { + if (self.appEnvironment != BITEnvironmentOther) return; + if ([self isUpdateManagerDisabled]) return; + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(shouldDisplayUpdateAlertForUpdateManager:forShortVersion:forVersion:)] && + ![strongDelegate shouldDisplayUpdateAlertForUpdateManager:self forShortVersion:[self.newestAppVersion shortVersion] forVersion:[self.newestAppVersion version]]) { + return; + } + + if (!self.updateAlertShowing) { + NSString *title = BITHockeyLocalizedString(@"UpdateAvailable"); + NSString *message = [NSString stringWithFormat:BITHockeyLocalizedString(@"UpdateAlertMandatoryTextWithAppVersion"), [self.newestAppVersion nameAndVersionString]]; + if ([self hasNewerMandatoryVersion]) { + __weak typeof(self) weakSelf = self; + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *showAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"UpdateShow") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + self.updateAlertShowing = NO; + if (strongSelf.blockingView) { + [strongSelf.blockingView removeFromSuperview]; + } + [strongSelf showUpdateView]; + }]; + [alertController addAction:showAction]; + UIAlertAction *installAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"UpdateInstall") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + self.updateAlertShowing = NO; + (void)[strongSelf initiateAppDownload]; + }]; + [alertController addAction:installAction]; + [self showAlertController:alertController]; + self.updateAlertShowing = YES; + } else { + message = [NSString stringWithFormat:BITHockeyLocalizedString(@"UpdateAlertTextWithAppVersion"), [self.newestAppVersion nameAndVersionString]]; + __weak typeof(self) weakSelf = self; + + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *ignoreAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"UpdateIgnore") + style:UIAlertActionStyleCancel + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + self.updateAlertShowing = NO; + if ([strongSelf expiryDateReached] && !strongSelf.blockingView) { + [strongSelf alertFallback:self.blockingScreenMessage]; + } + }]; + [alertController addAction:ignoreAction]; + UIAlertAction *showAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"UpdateShow") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + self.updateAlertShowing = NO; + if (strongSelf.blockingView) { + [strongSelf.blockingView removeFromSuperview]; + } + [strongSelf showUpdateView]; + }]; + [alertController addAction:showAction]; + if (self.isShowingDirectInstallOption) { + UIAlertAction *installAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"UpdateInstall") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + self.updateAlertShowing = NO; + (void)[strongSelf initiateAppDownload]; + }]; + [alertController addAction:installAction]; + } + [self showAlertController:alertController ]; + self.updateAlertShowing = YES; + } + } +} + + +// open an authorization screen +- (void)showBlockingScreen:(NSString *)message image:(NSString *)image { + self.blockingView = nil; + + UIWindow *visibleWindow = [self findVisibleWindow]; + if (visibleWindow == nil) { + [self alertFallback:message]; + return; + } + + CGRect frame = [visibleWindow frame]; + + self.blockingView = [[UIView alloc] initWithFrame:frame]; + UIImageView *backgroundView = [[UIImageView alloc] initWithImage:bit_imageNamed(@"bg.png", BITHOCKEYSDK_BUNDLE)]; + backgroundView.contentMode = UIViewContentModeScaleAspectFill; + backgroundView.frame = frame; + [self.blockingView addSubview:backgroundView]; + + if (image != nil) { + UIImageView *imageView = [[UIImageView alloc] initWithImage:bit_imageNamed(image, BITHOCKEYSDK_BUNDLE)]; + imageView.contentMode = UIViewContentModeCenter; + imageView.frame = frame; + [self.blockingView addSubview:imageView]; + } + + if (!self.disableUpdateCheckOptionWhenExpired) { + UIButton *checkForUpdateButton = [UIButton buttonWithType:kBITButtonTypeSystem]; + checkForUpdateButton.frame = CGRectMake((frame.size.width - 140) / (CGFloat)2.0, frame.size.height - 100, 140, 25); + [checkForUpdateButton setTitle:BITHockeyLocalizedString(@"UpdateButtonCheck") forState:UIControlStateNormal]; + [checkForUpdateButton addTarget:self + action:@selector(checkForUpdateForExpiredVersion) + forControlEvents:UIControlEventTouchUpInside]; + [self.blockingView addSubview:checkForUpdateButton]; + } + + if (message != nil) { + frame.origin.x = 20; + frame.origin.y = frame.size.height - 180; + frame.size.width -= 40; + frame.size.height = 70; + + UILabel *label = [[UILabel alloc] initWithFrame:frame]; + label.text = message; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 3; + label.adjustsFontSizeToFitWidth = YES; + label.backgroundColor = [UIColor clearColor]; + + [self.blockingView addSubview:label]; + } + + [visibleWindow addSubview:self.blockingView]; +} + +- (void)checkForUpdateForExpiredVersion { + if (!self.checkInProgress) { + + if (!self.lastUpdateCheckFromBlockingScreen || + fabs([NSDate timeIntervalSinceReferenceDate] - [self.lastUpdateCheckFromBlockingScreen timeIntervalSinceReferenceDate]) > 60) { + self.lastUpdateCheckFromBlockingScreen = [NSDate date]; + [self checkForUpdateShowFeedback:NO]; + } + } +} + +// nag the user with neverending alerts if we cannot find out the window for presenting the covering sheet +- (void)alertFallback:(NSString *)message { + __weak typeof(self) weakSelf = self; + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil + message:message + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *okAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyOK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + [strongSelf alertFallback:self.blockingScreenMessage]; + }]; + [alertController addAction:okAction]; + if (!self.disableUpdateCheckOptionWhenExpired && [message isEqualToString:self.blockingScreenMessage]) { + UIAlertAction *checkAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"UpdateButtonCheck") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + [strongSelf checkForUpdateForExpiredVersion]; + }]; + [alertController addAction:checkAction]; + } + [self showAlertController:alertController]; +} + +#pragma mark - RequestComments + +- (BOOL)shouldCheckForUpdates { + BOOL checkForUpdate = NO; + + switch (self.updateSetting) { + case BITUpdateCheckStartup: + checkForUpdate = YES; + break; + case BITUpdateCheckDaily: { + NSTimeInterval dateDiff = fabs([self.lastCheck timeIntervalSinceNow]); + if (dateDiff != 0) + dateDiff = dateDiff / (60*60*24); + + checkForUpdate = (dateDiff >= 1); + break; + } + case BITUpdateCheckManually: + checkForUpdate = NO; + break; + } + + return checkForUpdate; +} + +- (void)checkForUpdate { + if ((self.appEnvironment == BITEnvironmentOther) && ![self isUpdateManagerDisabled]) { + if ([self expiryDateReached]) return; + if (![self installationIdentified]) return; + + if (self.isUpdateAvailable && [self hasNewerMandatoryVersion]) { + [self showCheckForUpdateAlert]; + } + + [self checkForUpdateShowFeedback:NO]; + } +} + +- (void)checkForUpdateShowFeedback:(BOOL)feedback { + if (self.appEnvironment != BITEnvironmentOther) return; + if (self.isCheckInProgress) return; + + self.showFeedback = feedback; + self.checkInProgress = YES; + + // do we need to update? + if (!self.currentHockeyViewController && ![self shouldCheckForUpdates] && self.updateSetting != BITUpdateCheckManually) { + BITHockeyLogDebug(@"INFO: Update not needed right now"); + self.checkInProgress = NO; + return; + } + + NSURLRequest *request = [self requestForUpdateCheck]; + NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:(id)self delegateQueue:nil]; + + NSURLSessionDataTask *sessionTask = [session dataTaskWithRequest:request]; + if (!sessionTask) { + self.checkInProgress = NO; + [self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain + code:BITUpdateAPIClientCannotCreateConnection + userInfo:@{NSLocalizedDescriptionKey : @"Url Connection could not be created."}]]; + } else { + [sessionTask resume]; + } +} + +- (NSURLRequest *)requestForUpdateCheck { + NSString *path = [NSString stringWithFormat:@"api/2/apps/%@", self.appIdentifier]; + NSString *urlEncodedPath = [path stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLPathAllowedCharacterSet]]; + + NSMutableString *parameters = [NSMutableString stringWithFormat:@"?format=json&extended=true&sdk=%@&sdk_version=%@&uuid=%@", + BITHOCKEY_NAME, + BITHOCKEY_VERSION, + self.uuid]; + + // add installationIdentificationType and installationIdentifier if available + if (self.installationIdentification && self.installationIdentificationType) { + [parameters appendFormat:@"&%@=%@", + self.installationIdentificationType, + self.installationIdentification + ]; + } + + // add additional statistics if user didn't disable flag + if (self.sendUsageData) { + [parameters appendFormat:@"&app_version=%@&os=iOS&os_version=%@&device=%@&lang=%@&first_start_at=%@&usage_time=%@", + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"], + [[UIDevice currentDevice] systemVersion], + [self getDevicePlatform], + [[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0], + [self installationDateString], + [self currentUsageString] + ]; + } + NSString *urlEncodedParameters = [parameters stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + + // build request & send + NSString *url = [NSString stringWithFormat:@"%@%@%@", self.serverURL, urlEncodedPath, urlEncodedParameters]; + BITHockeyLogDebug(@"INFO: Sending api request to %@", url); + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:(NSURL *)[NSURL URLWithString:url] + cachePolicy:NSURLRequestReloadIgnoringLocalCacheData + timeoutInterval:10.0]; + [request setHTTPMethod:@"GET"]; + [request setValue:@"Hockey/iOS" forHTTPHeaderField:@"User-Agent"]; + [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"]; + + return request; +} + +- (BOOL)initiateAppDownload { + if (self.appEnvironment != BITEnvironmentOther) return NO; + + if (!self.isUpdateAvailable) { + BITHockeyLogWarning(@"WARNING: No update available. Aborting."); + return NO; + } + +#if TARGET_OS_SIMULATOR + + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:BITHockeyLocalizedString(@"UpdateWarning") + message:BITHockeyLocalizedString(@"UpdateSimulatorMessage") + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *okAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyOK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) {}]; + [alertController addAction:okAction]; + [self showAlertController:alertController]; + return NO; + +#else + + NSString *extraParameter = [NSString string]; + if (self.sendUsageData && self.installationIdentification && self.installationIdentificationType) { + extraParameter = [NSString stringWithFormat:@"&%@=%@", + bit_URLEncodedString(self.installationIdentificationType), + bit_URLEncodedString(self.installationIdentification) + ]; + } + + NSString *hockeyAPIURL = [NSString stringWithFormat:@"%@api/2/apps/%@/app_versions/%@?format=plist%@", self.serverURL, [self encodedAppIdentifier], [self.newestAppVersion.versionID stringValue], extraParameter]; + NSString *iOSUpdateURL = [NSString stringWithFormat:@"itms-services://?action=download-manifest&url=%@", bit_URLEncodedString(hockeyAPIURL)]; + + // Notify delegate of update intent before placing the call + id stronDelegate = self.delegate; + if ([stronDelegate respondsToSelector:@selector(willStartDownloadAndUpdate:)]) { + [stronDelegate willStartDownloadAndUpdate:self]; + } + + BITHockeyLogDebug(@"INFO: API Server Call: %@, calling iOS with %@", hockeyAPIURL, iOSUpdateURL); + BOOL success = [[UIApplication sharedApplication] openURL:(NSURL*)[NSURL URLWithString:iOSUpdateURL]]; + BITHockeyLogDebug(@"INFO: System returned: %d", success); + + self.didStartUpdateProcess = success; + + return success; + +#endif /* TARGET_OS_SIMULATOR */ +} + + +// begin the startup process +- (void)startManager { + if (self.appEnvironment == BITEnvironmentOther) { + if ([self isUpdateManagerDisabled]) return; + + BITHockeyLogDebug(@"INFO: Starting UpdateManager"); + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(updateManagerShouldSendUsageData:)]) { + self.sendUsageData = [strongDelegate updateManagerShouldSendUsageData:self]; + } + + [self checkExpiryDateReached]; + if (![self expiryDateReached]) { + if ([self isCheckForUpdateOnLaunch] && [self shouldCheckForUpdates]) { + if ([BITHockeyHelper applicationState] != BITApplicationStateActive) return; + + [self performSelector:@selector(checkForUpdate) withObject:nil afterDelay:1.0]; + } + } + } + [self registerObservers]; +} + +#pragma mark - Handle responses + +- (void)handleError:(NSError *)error { + self.receivedData = nil; + self.checkInProgress = NO; + if ([self expiryDateReached]) { + if (!self.blockingView) { + [self alertFallback:self.blockingScreenMessage]; + } + } else { + [self reportError:error]; + } +} + +- (void)finishLoading { + { + self.checkInProgress = NO; + + if ([self.receivedData length]) { + NSString *responseString = [[NSString alloc] initWithBytes:[self.receivedData bytes] length:[self.receivedData length] encoding: NSUTF8StringEncoding]; + BITHockeyLogDebug(@"INFO: Received API response: %@", responseString); + + if (!responseString || ![responseString dataUsingEncoding:NSUTF8StringEncoding]) { + self.receivedData = nil; + return; + } + + NSError *error = nil; + NSDictionary *json = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:(NSData *)[responseString dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&error]; + + self.companyName = (([[json valueForKey:@"company"] isKindOfClass:[NSString class]]) ? [json valueForKey:@"company"] : nil); + + if (self.appEnvironment == BITEnvironmentOther) { + NSArray *feedArray = (NSArray *)[json valueForKey:@"versions"]; + + // remember that we just checked the server + self.lastCheck = [NSDate date]; + + // server returned empty response? + if (![feedArray count]) { + BITHockeyLogDebug(@"WARNING: No versions available for download on HockeyApp."); + self.receivedData = nil; + return; + } else { + self.lastCheckFailed = NO; + } + + + NSString *currentAppCacheVersion = [[self newestAppVersion].version copy]; + + // clear cache and reload with new data + NSMutableArray *tmpAppVersions = [NSMutableArray arrayWithCapacity:[feedArray count]]; + for (NSDictionary *dict in feedArray) { + BITAppVersionMetaInfo *appVersionMetaInfo = [BITAppVersionMetaInfo appVersionMetaInfoFromDict:dict]; + if ([appVersionMetaInfo isValid]) { + // check if minOSVersion is set and this device qualifies + BOOL deviceOSVersionQualifies = YES; + if ([appVersionMetaInfo minOSVersion] && ![[appVersionMetaInfo minOSVersion] isKindOfClass:[NSNull class]]) { + NSComparisonResult comparisonResult = bit_versionCompare(appVersionMetaInfo.minOSVersion, [[UIDevice currentDevice] systemVersion]); + if (comparisonResult == NSOrderedDescending) { + deviceOSVersionQualifies = NO; + } + } + + if (deviceOSVersionQualifies) + [tmpAppVersions addObject:appVersionMetaInfo]; + } else { + [self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain + code:BITUpdateAPIServerReturnedInvalidData + userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Invalid data received from server.", NSLocalizedDescriptionKey, nil]]]; + } + } + // only set if different! + if (![self.appVersions isEqualToArray:tmpAppVersions]) { + self.appVersions = [tmpAppVersions copy]; + } + [self saveAppCache]; + + [self checkUpdateAvailable]; + BOOL newVersionDiffersFromCachedVersion = ![self.newestAppVersion.version isEqualToString:currentAppCacheVersion]; + + // show alert if we are on the latest & greatest + if (self.showFeedback && !self.isUpdateAvailable) { + // use currentVersionString, as version still may differ (e.g. server: 1.2, client: 1.3) + NSString *versionString = [self currentAppVersion]; + NSString *shortVersionString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + shortVersionString = shortVersionString ? [NSString stringWithFormat:@"%@ ", shortVersionString] : @""; + versionString = [shortVersionString length] ? [NSString stringWithFormat:@"(%@)", versionString] : versionString; + NSString *currentVersionString = [NSString stringWithFormat:@"%@ %@ %@%@", self.newestAppVersion.name, BITHockeyLocalizedString(@"UpdateVersion"), shortVersionString, versionString]; + NSString *alertMsg = [NSString stringWithFormat:BITHockeyLocalizedString(@"UpdateNoUpdateAvailableMessage"), currentVersionString]; + __weak typeof(self) weakSelf = self; + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:BITHockeyLocalizedString(@"UpdateNoUpdateAvailableTitle") + message:alertMsg + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *okAction = [BITAlertAction actionWithTitle:BITHockeyLocalizedString(@"HockeyOK") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction __unused *action) { + typeof(self) strongSelf = weakSelf; + self.updateAlertShowing = NO; + if ([strongSelf expiryDateReached] && !strongSelf.blockingView) { + [strongSelf alertFallback:self.blockingScreenMessage]; + } + }]; + [alertController addAction:okAction]; + [self showAlertController:alertController]; + } + + if (self.isUpdateAvailable && (self.alwaysShowUpdateReminder || newVersionDiffersFromCachedVersion || [self hasNewerMandatoryVersion])) { + if (self.updateAvailable && !self.currentHockeyViewController) { + [self showCheckForUpdateAlert]; + } + } + self.showFeedback = NO; + } + } else if (![self expiryDateReached]) { + [self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain + code:BITUpdateAPIServerReturnedEmptyResponse + userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Server returned an empty response.", NSLocalizedDescriptionKey, nil]]]; + } + + if (!self.updateAlertShowing && [self expiryDateReached] && !self.blockingView) { + [self alertFallback:self.blockingScreenMessage]; + } + + self.receivedData = nil; + } +} + +#pragma mark - NSURLSession + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *) __unused task didCompleteWithError:(NSError *)error { + + dispatch_async(dispatch_get_main_queue(), ^{ + [session finishTasksAndInvalidate]; + + if(error){ + [self handleError:error]; + }else{ + [self finishLoading]; + } + }); +} + +- (void)URLSession:(NSURLSession *) __unused session dataTask:(NSURLSessionDataTask *) __unused dataTask didReceiveData:(NSData *)data { + [self.receivedData appendData:data]; +} + +- (void)URLSession:(NSURLSession *) __unused session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { + + if ([response respondsToSelector:@selector(statusCode)]) { + NSInteger statusCode = [((NSHTTPURLResponse *)response) statusCode]; + if (statusCode == 404) { + [dataTask cancel]; + NSString *errorStr = [NSString stringWithFormat:@"Hockey API received HTTP Status Code %ld", (long)statusCode]; + [self reportError:[NSError errorWithDomain:kBITUpdateErrorDomain + code:BITUpdateAPIServerReturnedInvalidStatus + userInfo:[NSDictionary dictionaryWithObjectsAndKeys:errorStr, NSLocalizedDescriptionKey, nil]]]; + if (completionHandler) { completionHandler(NSURLSessionResponseCancel); } + return; + } + if (completionHandler) { completionHandler(NSURLSessionResponseAllow);} + } + + self.receivedData = [NSMutableData data]; + [self.receivedData setLength:0]; +} + +- (void)URLSession:(NSURLSession *) __unused session task:(NSURLSessionTask *) __unused task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler { + NSURLRequest *newRequest = request; + if (response) { + newRequest = nil; + } + if (completionHandler) { completionHandler(newRequest); } +} + +- (BOOL)hasNewerMandatoryVersion { + BOOL result = NO; + + for (BITAppVersionMetaInfo *appVersion in self.appVersions) { + if ([appVersion.version isEqualToString:self.currentAppVersion] || bit_versionCompare(appVersion.version, self.currentAppVersion) == NSOrderedAscending) { + break; + } + + if ([appVersion.mandatory boolValue]) { + result = YES; + } + } + + return result; +} + +#pragma mark - Properties + +- (void)setCurrentHockeyViewController:(BITUpdateViewController *)aCurrentHockeyViewController { + if (_currentHockeyViewController != aCurrentHockeyViewController) { + _currentHockeyViewController = aCurrentHockeyViewController; + //HockeySDKLog(@"active hockey view controller: %@", aCurrentHockeyViewController); + } +} + +- (NSString *)currentAppVersion { + return _currentAppVersion; +} + +- (void)setLastCheck:(NSDate *)aLastCheck { + if (_lastCheck != aLastCheck) { + _lastCheck = [aLastCheck copy]; + + [[NSUserDefaults standardUserDefaults] setObject:_lastCheck forKey:kBITUpdateDateOfLastCheck]; + } +} + +- (void)setAppVersions:(NSArray *)anAppVersions { + if (_appVersions != anAppVersions || !_appVersions) { + [self willChangeValueForKey:@"appVersions"]; + + // populate with default values (if empty) + if (![anAppVersions count]) { + BITAppVersionMetaInfo *defaultApp = [[BITAppVersionMetaInfo alloc] init]; + defaultApp.name = bit_appName(BITHockeyLocalizedString(@"HockeyAppNamePlaceholder")); + defaultApp.version = self.currentAppVersion; + defaultApp.shortVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + _appVersions = [NSArray arrayWithObject:defaultApp]; + } else { + _appVersions = [anAppVersions copy]; + } + [self didChangeValueForKey:@"appVersions"]; + } +} + +- (BITAppVersionMetaInfo *)newestAppVersion { + BITAppVersionMetaInfo *appVersion = [self.appVersions objectAtIndex:0]; + return appVersion; +} + +- (void)setBlockingView:(UIView *)anBlockingView { + if (_blockingView != anBlockingView) { + [_blockingView removeFromSuperview]; + _blockingView = anBlockingView; + } +} + +- (void)setInstallationIdentificationType:(NSString *)installationIdentificationType { + if (![_installationIdentificationType isEqualToString:installationIdentificationType]) { + // we already use "uuid" in our requests for providing the binary UUID to the server + // so we need to stick to "udid" even when BITAuthenticator is providing a plain uuid + if ([installationIdentificationType isEqualToString:@"uuid"]) { + _installationIdentificationType = @"udid"; + } else { + _installationIdentificationType = installationIdentificationType; + } + } +} + +- (void)setInstallationIdentification:(NSString *)installationIdentification { + if (![_installationIdentification isEqualToString:installationIdentification]) { + if (installationIdentification) { + [self addStringValueToKeychain:installationIdentification forKey:kBITUpdateInstallationIdentification]; + } else { + [self removeKeyFromKeychain:kBITUpdateInstallationIdentification]; + } + _installationIdentification = installationIdentification; + + // we need to reset the usage time, because the user/device may have changed + [self storeUsageTimeForCurrentVersion:[NSNumber numberWithDouble:0]]; + self.usageStartTimestamp = [NSDate date]; + } +} + +@end + +#endif /* HOCKEYSDK_FEATURE_UPDATES */ diff --git a/submodules/HockeySDK-iOS/Classes/BITUpdateManagerDelegate.h b/submodules/HockeySDK-iOS/Classes/BITUpdateManagerDelegate.h new file mode 100644 index 0000000000..80c1506138 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITUpdateManagerDelegate.h @@ -0,0 +1,160 @@ +/* + * 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 + +@class BITUpdateManager; + +/** + The `BITUpdateManagerDelegate` formal protocol defines methods further configuring + the behaviour of `BITUpdateManager`. + */ + +@protocol BITUpdateManagerDelegate + +@optional + +///----------------------------------------------------------------------------- +/// @name Update +///----------------------------------------------------------------------------- + +/** + Return if update alert should be shown + + If you want to display your own user interface when there is an update available, + implement this method, present your user interface and return _NO_. In this case + it is your responsibility to call `BITUpdateManager showUpdateView` + + Note: This delegate will be invoked on startup and every time the app becomes + active again! + + When returning _YES_ the default blocking UI will be shown. + + When running the app from the App Store, this delegate is ignored. + + @param updateManager The `BITUpdateManager` instance invoking this delegate + @param shortVersion The latest available version + @param version The latest available version + */ +- (BOOL)shouldDisplayUpdateAlertForUpdateManager:(BITUpdateManager *)updateManager forShortVersion:(NSString *)shortVersion forVersion:(NSString *)version; + +///----------------------------------------------------------------------------- +/// @name Expiry +///----------------------------------------------------------------------------- + +/** + Return if expiry alert should be shown if date is reached + + If you want to display your own user interface when the expiry date is reached, + implement this method, present your user interface and return _NO_. In this case + it is your responsibility to make the app unusable! + + Note: This delegate will be invoked on startup and every time the app becomes + active again! + + When returning _YES_ the default blocking UI will be shown. + + When running the app from the App Store, this delegate is ignored. + + @param updateManager The `BITUpdateManager` instance invoking this delegate + @see [BITUpdateManager expiryDate] + @see [BITUpdateManagerDelegate didDisplayExpiryAlertForUpdateManager:] + */ +- (BOOL)shouldDisplayExpiryAlertForUpdateManager:(BITUpdateManager *)updateManager; + + +/** + Invoked once a default expiry alert is shown + + Once expiry date is reached and the default blocking UI is shown, + this delegate method is invoked to provide you the possibility to do any + desired additional processing. + + @param updateManager The `BITUpdateManager` instance invoking this delegate + @see [BITUpdateManager expiryDate] + @see [BITUpdateManagerDelegate shouldDisplayExpiryAlertForUpdateManager:] + */ +- (void)didDisplayExpiryAlertForUpdateManager:(BITUpdateManager *)updateManager; + + +///----------------------------------------------------------------------------- +/// @name Privacy +///----------------------------------------------------------------------------- + +/** Return NO if usage data should not be send + + The update module send usage data by default, if the application is _NOT_ + running in an App Store version. Implement this delegate and + return NO if you want to disable this. + + If you intend to implement a user setting to let them enable or disable + sending usage data, this delegate should be used to return that value. + + Usage data contains the following information: + - App Version + - iOS Version + - Device type + - Language + - Installation timestamp + - Usage time + + @param updateManager The `BITUpdateManager` instance invoking this delegate + @warning When setting this to `NO`, you will _NOT_ know if this user is actually testing! + */ +- (BOOL)updateManagerShouldSendUsageData:(BITUpdateManager *)updateManager; + + +///----------------------------------------------------------------------------- +/// @name Privacy +///----------------------------------------------------------------------------- + +/** Implement this method to be notified before an update starts. + + The update manager will send this delegate message _just_ before the system + call to update the application is placed, but after the user has already chosen + to install the update. + + There is no guarantee that the update will actually start after this delegate + message is sent. + + @param updateManager The `BITUpdateManager` instance invoking this delegate + */ +- (BOOL)willStartDownloadAndUpdate:(BITUpdateManager *)updateManager; + +/** + Invoked right before the app will exit to allow app update to start (>= iOS8 only) + + The iOS installation mechanism only starts if the app the should be updated is currently + not running. On all iOS versions up to iOS 7, the system did automatically exit the app + in these cases. Since iOS 8 this isn't done any longer. + + @param updateManager The `BITUpdateManager` instance invoking this delegate + */ +- (void)updateManagerWillExitApp:(BITUpdateManager *)updateManager; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITUpdateManagerPrivate.h b/submodules/HockeySDK-iOS/Classes/BITUpdateManagerPrivate.h new file mode 100644 index 0000000000..f578247a0a --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITUpdateManagerPrivate.h @@ -0,0 +1,110 @@ +/* + * Author: Andreas Linde + * Peter Steinberger + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde. + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_UPDATES + +/** TODO: + * if during startup the auth-state is pending, we get never rid of the nag-alertview + */ +@interface BITUpdateManager () + +///----------------------------------------------------------------------------- +/// @name Delegate +///----------------------------------------------------------------------------- + +/** + Sets the `BITUpdateManagerDelegate` delegate. + + The delegate is automatically set by using `[BITHockeyManager setDelegate:]`. You + should not need to set this delegate individually. + + @see `[BITHockeyManager setDelegate:]` + */ +@property (nonatomic, weak) id delegate; + + +// is an update available? +@property (nonatomic, assign, getter=isUpdateAvailable) BOOL updateAvailable; + +// are we currently checking for updates? +@property (nonatomic, assign, getter=isCheckInProgress) BOOL checkInProgress; + +@property (nonatomic, strong) NSMutableData *receivedData; + +@property (nonatomic, copy) NSDate *lastCheck; + +// get array of all available versions +@property (nonatomic, copy) NSArray *appVersions; + +@property (nonatomic, strong) NSNumber *currentAppVersionUsageTime; + +@property (nonatomic, copy) NSDate *usageStartTimestamp; + +@property (nonatomic, strong) UIView *blockingView; + +@property (nonatomic, copy) NSString *companyName; + +@property (nonatomic, copy) NSString *installationIdentification; + +@property (nonatomic, copy) NSString *installationIdentificationType; + +@property (nonatomic) BOOL installationIdentified; + +// used by BITHockeyManager if disable status is changed +@property (nonatomic, getter = isUpdateManagerDisabled) BOOL disableUpdateManager; + +// checks for update, informs the user (error, no update found, etc) +- (void)checkForUpdateShowFeedback:(BOOL)feedback; + +- (NSURLRequest *)requestForUpdateCheck; + +// initiates app-download call. displays an system UIAlertController +- (BOOL)initiateAppDownload; + +// get/set current active hockey view controller +@property (nonatomic, strong) BITUpdateViewController *currentHockeyViewController; + +@property(nonatomic) BOOL sendUsageData; + +// convenience method to get current running version string +- (NSString *)currentAppVersion; + +// get newest app version +- (BITAppVersionMetaInfo *)newestAppVersion; + +// check if there is any newer version mandatory +- (BOOL)hasNewerMandatoryVersion; + +@end + +#endif /* HOCKEYSDK_FEATURE_UPDATES */ diff --git a/submodules/HockeySDK-iOS/Classes/BITUpdateViewController.h b/submodules/HockeySDK-iOS/Classes/BITUpdateViewController.h new file mode 100644 index 0000000000..e36f318270 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITUpdateViewController.h @@ -0,0 +1,37 @@ +/* + * Author: Andreas Linde + * Peter Steinberger + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde, Peter Steinberger. + * 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 + +#import "BITHockeyBaseViewController.h" + + +@interface BITUpdateViewController : BITHockeyBaseViewController +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITUpdateViewController.m b/submodules/HockeySDK-iOS/Classes/BITUpdateViewController.m new file mode 100644 index 0000000000..ee9ab55d21 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITUpdateViewController.m @@ -0,0 +1,540 @@ +/* + * Author: Andreas Linde + * Peter Steinberger + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde, Peter Steinberger. + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_UPDATES + +#import "HockeySDKPrivate.h" +#import +#import "BITHockeyHelper.h" +#import "BITAppVersionMetaInfo.h" +#import "BITAppStoreHeader.h" +#import "BITWebTableViewCell.h" +#import "BITStoreButton.h" + +#import "BITUpdateManagerPrivate.h" +#import "BITUpdateViewControllerPrivate.h" +#import "BITHockeyBaseManagerPrivate.h" + + +#define kWebCellIdentifier @"PSWebTableViewCell" +#define kAppStoreViewHeight 99 + +@interface BITUpdateViewController () + +@property (nonatomic) BOOL kvoRegistered; +@property (nonatomic) BOOL showAllVersions; +@property (nonatomic, strong) BITAppStoreHeader *appStoreHeader; +@property (nonatomic, strong) BITStoreButton *appStoreButton; +@property (nonatomic, strong) id popOverController; +@property (nonatomic, strong) NSMutableArray *cells; +@property (nonatomic) BITEnvironment appEnvironment; + +@end + +@implementation BITUpdateViewController + +#pragma mark - Private + +- (UIColor *)backgroundColor { + return BIT_RGBCOLOR(255, 255, 255); +} + +- (void)restoreStoreButtonStateAnimated:(BOOL)animated { + if (self.appEnvironment == BITEnvironmentAppStore) { + [self setAppStoreButtonState:AppStoreButtonStateOffline animated:animated]; + } else if ([self.updateManager isUpdateAvailable]) { + [self setAppStoreButtonState:AppStoreButtonStateUpdate animated:animated]; + } else { + [self setAppStoreButtonState:AppStoreButtonStateCheck animated:animated]; + } +} + +- (void)updateAppStoreHeader { + BITUpdateManager *strongManager = self.updateManager; + BITAppVersionMetaInfo *appVersion = strongManager.newestAppVersion; + self.appStoreHeader.headerText = appVersion.name; + self.appStoreHeader.subHeaderText = strongManager.companyName; +} + +- (void)appDidBecomeActive { + if (self.appStoreButtonState == AppStoreButtonStateInstalling) { + [self setAppStoreButtonState:AppStoreButtonStateUpdate animated:YES]; + } else if (![self.updateManager isCheckInProgress]) { + [self restoreStoreButtonStateAnimated:YES]; + } +} + +- (UIImage *)addGlossToImage:(UIImage *)image { + UIGraphicsBeginImageContextWithOptions(image.size, NO, 0.0); + + [image drawAtPoint:CGPointZero]; + UIImage *iconGradient = bit_imageNamed(@"IconGradient.png", BITHOCKEYSDK_BUNDLE); + [iconGradient drawInRect:CGRectMake(0, 0, image.size.width, image.size.height) blendMode:kCGBlendModeNormal alpha:0.5]; + + UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return result; +} + +#define kMinPreviousVersionButtonHeight 50 +- (void)realignPreviousVersionButton { + + // manually collect actual table height size + NSUInteger tableViewContentHeight = 0; + for (int i=0; i < [self tableView:self.tableView numberOfRowsInSection:0]; i++) { + tableViewContentHeight += [self tableView:self.tableView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]]; + } + tableViewContentHeight += self.tableView.tableHeaderView.frame.size.height; + tableViewContentHeight += self.navigationController.navigationBar.frame.size.height; + tableViewContentHeight += [UIApplication sharedApplication].statusBarFrame.size.height; + + NSUInteger footerViewSize = kMinPreviousVersionButtonHeight; + NSUInteger frameHeight = (NSUInteger)self.view.frame.size.height; + if(tableViewContentHeight < frameHeight && (frameHeight - tableViewContentHeight > 100)) { + footerViewSize = frameHeight - tableViewContentHeight; + } + + // update footer view + if(self.tableView.tableFooterView) { + CGRect frame = self.tableView.tableFooterView.frame; + frame.size.height = footerViewSize; + self.tableView.tableFooterView.frame = frame; + } +} + +- (void)changePreviousVersionButtonBackground:(id)sender { + [(UIButton *)sender setBackgroundColor:BIT_RGBCOLOR(245, 245, 245)]; +} + +- (void)changePreviousVersionButtonBackgroundHighlighted:(id)sender { + [(UIButton *)sender setBackgroundColor:BIT_RGBCOLOR(245, 245, 245)]; +} + +- (UIImage *)gradientButtonHighlightImage { + CGFloat width = 10; + CGFloat height = 70; + + CGSize size = CGSizeMake(width, height); + UIGraphicsBeginImageContextWithOptions(size, NO, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); + + NSArray *colors = [NSArray arrayWithObjects:(id)BIT_RGBCOLOR(69, 127, 247).CGColor, (id)BIT_RGBCOLOR(58, 68, 233).CGColor, nil]; + CGGradientRef gradient = CGGradientCreateWithColors(CGColorGetColorSpace((__bridge CGColorRef)[colors objectAtIndex:0]), (__bridge CFArrayRef)colors, (CGFloat[2]){0, 1}); + CGPoint top = CGPointMake(width / 2, 0); + CGPoint bottom = CGPointMake(width / 2, height); + CGContextDrawLinearGradient(context, gradient, top, bottom, 0); + + UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext(); + + CGGradientRelease(gradient); + CGColorSpaceRelease(colorspace); + UIGraphicsEndImageContext(); + + return theImage; +} + +- (void)showHidePreviousVersionsButton { + BOOL multipleVersionButtonNeeded = [self.updateManager.appVersions count] > 1 && !self.showAllVersions; + + if(multipleVersionButtonNeeded) { + // align at the bottom if tableview is small + UIView *footerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, kMinPreviousVersionButtonHeight)]; + footerView.autoresizingMask = UIViewAutoresizingFlexibleWidth; + footerView.backgroundColor = BIT_RGBCOLOR(245, 245, 245); + UIView *lineView1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 1)]; + lineView1.backgroundColor = BIT_RGBCOLOR(214, 214, 214); + lineView1.autoresizingMask = UIViewAutoresizingFlexibleWidth; + [footerView addSubview:lineView1]; + UIView *lineView2 = [[UIView alloc] initWithFrame:CGRectMake(0, 1, self.view.frame.size.width, 1)]; + lineView2.backgroundColor = BIT_RGBCOLOR(221, 221, 221); + lineView2.autoresizingMask = UIViewAutoresizingFlexibleWidth; + [footerView addSubview:lineView2]; + UIView *lineView3 = [[UIView alloc] initWithFrame:CGRectMake(0, 1, self.view.frame.size.width, 1)]; + lineView3.backgroundColor = BIT_RGBCOLOR(255, 255, 255); + lineView3.autoresizingMask = UIViewAutoresizingFlexibleWidth; + [footerView addSubview:lineView3]; + UIButton *footerButton = [UIButton buttonWithType:UIButtonTypeCustom]; + //footerButton.layer.shadowOffset = CGSizeMake(-2, 2); + footerButton.layer.shadowColor = [[UIColor blackColor] CGColor]; + footerButton.layer.shadowRadius = 2.0; + footerButton.titleLabel.font = [UIFont boldSystemFontOfSize:14]; + [footerButton setTitle:BITHockeyLocalizedString(@"UpdateShowPreviousVersions") forState:UIControlStateNormal]; + [footerButton setTitleColor:BIT_RGBCOLOR(61, 61, 61) forState:UIControlStateNormal]; + [footerButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted]; + [footerButton setBackgroundImage:[self gradientButtonHighlightImage] forState:UIControlStateHighlighted]; + footerButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + [footerButton addTarget:self action:@selector(showPreviousVersionAction) forControlEvents:UIControlEventTouchUpInside]; + footerButton.frame = CGRectMake(0, kMinPreviousVersionButtonHeight-44, self.view.frame.size.width, 44); + footerButton.backgroundColor = BIT_RGBCOLOR(245, 245, 245); + [footerView addSubview:footerButton]; + self.tableView.tableFooterView = footerView; + [self realignPreviousVersionButton]; + } else { + self.tableView.tableFooterView = nil; + self.tableView.backgroundColor = [self backgroundColor]; + } +} + +- (void)configureWebCell:(BITWebTableViewCell *)cell forAppVersion:(BITAppVersionMetaInfo *)appVersion { + // create web view for a version + NSMutableString *dateAndSizeString = [NSMutableString string]; + if (appVersion.date) { + [dateAndSizeString appendString:[appVersion dateString]]; + } + if (appVersion.size) { + if ([dateAndSizeString length]) { + [dateAndSizeString appendString:@" - "]; + } + [dateAndSizeString appendString:appVersion.sizeInMB]; + } + + NSString *installed = @""; + BITUpdateManager *strongManager = self.updateManager; + if ([appVersion.version isEqualToString:[strongManager currentAppVersion]]) { + installed = [NSString stringWithFormat:@"%@", BITHockeyLocalizedString(@"UpdateInstalled")]; + } + + if ([appVersion isEqual:strongManager.newestAppVersion]) { + if ([appVersion.notes length] > 0) { + cell.webViewContent = [NSString stringWithFormat:@"

%@%@
%@

%@

", [appVersion versionString], installed, dateAndSizeString, appVersion.notes]; + } else { + cell.webViewContent = [NSString stringWithFormat:@"
%@
", BITHockeyLocalizedString(@"UpdateNoReleaseNotesAvailable")]; + } + } else { + cell.webViewContent = [NSString stringWithFormat:@"

%@%@
%@

%@

", [appVersion versionString], installed, dateAndSizeString, [appVersion notesOrEmptyString]]; + } + cell.cellBackgroundColor = [self backgroundColor]; + [cell addWebView]; + // hack + cell.textLabel.text = @""; + + [cell addObserver:self forKeyPath:@"webViewSize" options:0 context:nil]; +} + + +#pragma mark - Init + +- (instancetype)initWithStyle:(UITableViewStyle) __unused style { + if ((self = [super initWithStyle:UITableViewStylePlain])) { + self.updateManager = [BITHockeyManager sharedHockeyManager].updateManager ; + self.appEnvironment = [BITHockeyManager sharedHockeyManager].appEnvironment; + + self.title = BITHockeyLocalizedString(@"UpdateScreenTitle"); + + self.cells = [[NSMutableArray alloc] initWithCapacity:5]; + self.popOverController = nil; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + BITUpdateManager *strongManager = self.updateManager; + // test if KVO's are registered. if class is destroyed before it was shown(viewDidLoad) no KVOs are registered. + if (self.kvoRegistered) { + [strongManager removeObserver:self forKeyPath:@"checkInProgress"]; + [strongManager removeObserver:self forKeyPath:@"isUpdateURLOffline"]; + [strongManager removeObserver:self forKeyPath:@"updateAvailable"]; + [strongManager removeObserver:self forKeyPath:@"appVersions"]; + self.kvoRegistered = NO; + } + + for (UITableViewCell *cell in self.cells) { + [cell removeObserver:self forKeyPath:@"webViewSize"]; + } + +} + + +#pragma mark - View lifecycle + +- (void)viewDidLoad { + [super viewDidLoad]; + + // add notifications only to loaded view + NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter]; + [dnc addObserver:self selector:@selector(appDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; + + // hook into manager with kvo! + BITUpdateManager *strongManager = self.updateManager; + [strongManager addObserver:self forKeyPath:@"checkInProgress" options:0 context:nil]; + [strongManager addObserver:self forKeyPath:@"isUpdateURLOffline" options:0 context:nil]; + [strongManager addObserver:self forKeyPath:@"updateAvailable" options:0 context:nil]; + [strongManager addObserver:self forKeyPath:@"appVersions" options:0 context:nil]; + self.kvoRegistered = YES; + + self.tableView.backgroundColor = BIT_RGBCOLOR(245, 245, 245); + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + + UIView *topView = [[UIView alloc] initWithFrame:CGRectMake(0, -(600-kAppStoreViewHeight), self.view.frame.size.width, 600)]; + topView.autoresizingMask = UIViewAutoresizingFlexibleWidth; + topView.backgroundColor = BIT_RGBCOLOR(245, 245, 245); + [self.tableView addSubview:topView]; + + self.appStoreHeader = [[BITAppStoreHeader alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, kAppStoreViewHeight)]; + [self updateAppStoreHeader]; + + NSString *iconFilename = bit_validAppIconFilename([NSBundle mainBundle], [NSBundle mainBundle]); + if (iconFilename) { + self.appStoreHeader.iconImage = [UIImage imageNamed:iconFilename]; + } + + self.tableView.tableHeaderView = self.appStoreHeader; + + BITStoreButton *storeButton = [[BITStoreButton alloc] initWithPadding:CGPointMake(5, 58) style:BITStoreButtonStyleOS7]; + storeButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + storeButton.buttonDelegate = self; + [self.tableView.tableHeaderView addSubview:storeButton]; + storeButton.buttonData = [BITStoreButtonData dataWithLabel:@"" enabled:NO]; + [storeButton alignToSuperview]; + self.appStoreButton = storeButton; + self.appStoreButtonState = AppStoreButtonStateCheck; +} + +- (void)viewWillAppear:(BOOL)animated { + if (self.appEnvironment != BITEnvironmentOther) { + self.appStoreButtonState = AppStoreButtonStateOffline; + } else if (self.mandatoryUpdate) { + self.navigationItem.leftBarButtonItem = nil; + } + self.updateManager.currentHockeyViewController = self; + [super viewWillAppear:animated]; + [self redrawTableView]; +} + +- (void)viewWillDisappear:(BOOL)animated { + self.updateManager.currentHockeyViewController = nil; + //if the popover is still visible, dismiss it + [self.popOverController dismissPopoverAnimated:YES]; + [super viewWillDisappear:animated]; +} + +- (void)redrawTableView { + [self restoreStoreButtonStateAnimated:NO]; + [self updateAppStoreHeader]; + + // clean up and remove any pending observers + for (UITableViewCell *cell in self.cells) { + [cell removeObserver:self forKeyPath:@"webViewSize"]; + } + [self.cells removeAllObjects]; + + int i = 0; + BOOL breakAfterThisAppVersion = NO; + BITUpdateManager *stronManager = self.updateManager; + for (BITAppVersionMetaInfo *appVersion in stronManager.appVersions) { + i++; + + // only show the newer version of the app by default, if we don't show all versions + if (!self.showAllVersions) { + if ([appVersion.version isEqualToString:[stronManager currentAppVersion]]) { + if (i == 1) { + breakAfterThisAppVersion = YES; + } else { + break; + } + } + } + + BITWebTableViewCell *cell = [self webCellWithAppVersion:appVersion]; + [self.cells addObject:cell]; + + if (breakAfterThisAppVersion) break; + } + + [self.tableView reloadData]; + [self showHidePreviousVersionsButton]; +} + +- (BITWebTableViewCell *)webCellWithAppVersion:(BITAppVersionMetaInfo *)appVersion { + BITWebTableViewCell *cell = [[BITWebTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kWebCellIdentifier]; + [self configureWebCell:cell forAppVersion:appVersion]; + return cell; +} + +- (void)showPreviousVersionAction { + self.showAllVersions = YES; + BOOL showAllPending = NO; + BITUpdateManager *strongManager = self.updateManager; + for (BITAppVersionMetaInfo *appVersion in strongManager.appVersions) { + if (!showAllPending) { + if ([appVersion.version isEqualToString:[strongManager currentAppVersion]]) { + showAllPending = YES; + if (appVersion == strongManager.newestAppVersion) { + continue; // skip this version already if it the latest version is the installed one + } + } else { + continue; // skip already shown + } + } + + [self.cells addObject:[self webCellWithAppVersion:appVersion]]; + } + [self.tableView reloadData]; + [self showHidePreviousVersionsButton]; +} + + +#pragma mark - Table view data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView *) __unused tableView { + return 1; +} + +- (CGFloat)tableView:(UITableView *) __unused tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + CGFloat rowHeight = 0; + + if ([self.cells count] > (NSUInteger)indexPath.row) { + BITWebTableViewCell *cell = [self.cells objectAtIndex:indexPath.row]; + rowHeight = cell.webViewSize.height; + } + + if ([self.updateManager.appVersions count] > 1 && !self.showAllVersions) { + self.tableView.backgroundColor = BIT_RGBCOLOR(245, 245, 245); + } + + if (rowHeight == 0) { + rowHeight = indexPath.row == 0 ? 250 : 44; // fill screen on startup + self.tableView.backgroundColor = [self backgroundColor]; + } + + return rowHeight; +} + +- (NSInteger)tableView:(UITableView *) __unused tableView numberOfRowsInSection:(NSInteger) __unused section { + NSInteger cellCount = [self.cells count]; + return cellCount; +} + + +#pragma mark - KVO + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id) __unused object change:(NSDictionary *) __unused change context:(void *) __unused context { + // only make changes if we are visible + if(self.view.window) { + if ([keyPath isEqualToString:@"webViewSize"]) { + [self.tableView reloadData]; + [self realignPreviousVersionButton]; + } else if ([keyPath isEqualToString:@"checkInProgress"]) { + if (self.updateManager.isCheckInProgress) { + [self setAppStoreButtonState:AppStoreButtonStateSearching animated:YES]; + }else { + [self restoreStoreButtonStateAnimated:YES]; + } + } else if ([keyPath isEqualToString:@"isUpdateURLOffline"]) { + [self restoreStoreButtonStateAnimated:YES]; + } else if ([keyPath isEqualToString:@"updateAvailable"]) { + [self restoreStoreButtonStateAnimated:YES]; + } else if ([keyPath isEqualToString:@"appVersions"]) { + [self redrawTableView]; + } + } +} + +// Customize the appearance of table view cells. +- (UITableViewCell *)tableView:(UITableView *) __unused tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + if ([self.cells count] > (NSUInteger)indexPath.row) { + return [self.cells objectAtIndex:indexPath.row]; + } else { + BITHockeyLogWarning(@"Warning: cells_ and indexPath do not match? forgot calling redrawTableView? Returning empty UITableViewCell"); + return [UITableViewCell new]; + + } +} + + +#pragma mark - Rotation + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation) __unused interfaceOrientation duration:(NSTimeInterval) __unused duration { + // update all cells + [self.cells makeObjectsPerformSelector:@selector(addWebView)]; +} +#pragma clang diagnostic pop + +#pragma mark - PSAppStoreHeaderDelegate + +- (void)setAppStoreButtonState:(AppStoreButtonState)anAppStoreButtonState { + [self setAppStoreButtonState:anAppStoreButtonState animated:NO]; +} + +- (void)setAppStoreButtonState:(AppStoreButtonState)anAppStoreButtonState animated:(BOOL)animated { + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + _appStoreButtonState = anAppStoreButtonState; +#pragma clang diagnostic pop + + switch (anAppStoreButtonState) { + case AppStoreButtonStateOffline: + [self.appStoreButton setButtonData:[BITStoreButtonData dataWithLabel:BITHockeyLocalizedString(@"UpdateButtonOffline") enabled:NO] animated:animated]; + break; + case AppStoreButtonStateCheck: + [self.appStoreButton setButtonData:[BITStoreButtonData dataWithLabel:BITHockeyLocalizedString(@"UpdateButtonCheck") enabled:YES] animated:animated]; + break; + case AppStoreButtonStateSearching: + [self.appStoreButton setButtonData:[BITStoreButtonData dataWithLabel:BITHockeyLocalizedString(@"UpdateButtonSearching") enabled:NO] animated:animated]; + break; + case AppStoreButtonStateUpdate: + [self.appStoreButton setButtonData:[BITStoreButtonData dataWithLabel:BITHockeyLocalizedString(@"UpdateButtonUpdate") enabled:YES] animated:animated]; + break; + case AppStoreButtonStateInstalling: + [self.appStoreButton setButtonData:[BITStoreButtonData dataWithLabel:BITHockeyLocalizedString(@"UpdateButtonInstalling") enabled:NO] animated:animated]; + break; + default: + break; + } +} + +- (void)storeButtonFired:(BITStoreButton *) __unused button { + BITUpdateManager *strongManager = self.updateManager; + switch (self.appStoreButtonState) { + case AppStoreButtonStateCheck: + [strongManager checkForUpdateShowFeedback:YES]; + break; + case AppStoreButtonStateUpdate: + if ([strongManager initiateAppDownload]) { + [self setAppStoreButtonState:AppStoreButtonStateInstalling animated:YES]; + }; + break; + default: + break; + } +} + +@end + +#endif /* HOCKEYSDK_FEATURE_UPDATES */ diff --git a/submodules/HockeySDK-iOS/Classes/BITUpdateViewControllerPrivate.h b/submodules/HockeySDK-iOS/Classes/BITUpdateViewControllerPrivate.h new file mode 100644 index 0000000000..26747a8288 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITUpdateViewControllerPrivate.h @@ -0,0 +1,79 @@ +/* + * Author: Andreas Linde + * Peter Steinberger + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde, Peter Steinberger. + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_UPDATES + +#import "BITStoreButton.h" + +/** + * Button states + */ +typedef NS_ENUM(NSUInteger, AppStoreButtonState) { + /** + * Offline + */ + AppStoreButtonStateOffline, + /** + * Check + */ + AppStoreButtonStateCheck, + /** + * Searching + */ + AppStoreButtonStateSearching, + /** + * Update + */ + AppStoreButtonStateUpdate, + /** + * Installing + */ + AppStoreButtonStateInstalling +}; + + +@class BITUpdateManager; + +@protocol PSStoreButtonDelegate; + +@interface BITUpdateViewController() { +} + +@property (nonatomic, weak) BITUpdateManager *updateManager; + +@property (nonatomic, readwrite) BOOL mandatoryUpdate; + +@property (nonatomic, assign) AppStoreButtonState appStoreButtonState; + +@end + +#endif /* HOCKEYSDK_FEATURE_UPDATES */ diff --git a/submodules/HockeySDK-iOS/Classes/BITUser.h b/submodules/HockeySDK-iOS/Classes/BITUser.h new file mode 100755 index 0000000000..12450aa078 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITUser.h @@ -0,0 +1,16 @@ +#import "BITTelemetryObject.h" + +@interface BITUser : BITTelemetryObject + +@property (nonatomic, copy) NSString *accountAcquisitionDate; +@property (nonatomic, copy) NSString *accountId; +@property (nonatomic, copy) NSString *userAgent; +@property (nonatomic, copy) NSString *userId; +@property (nonatomic, copy) NSString *storeRegion; +@property (nonatomic, copy) NSString *authUserId; +@property (nonatomic, copy) NSString *anonUserAcquisitionDate; +@property (nonatomic, copy) NSString *authUserAcquisitionDate; + +- (BOOL)isEqualToUser:(BITUser *)aUser; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITUser.m b/submodules/HockeySDK-iOS/Classes/BITUser.m new file mode 100755 index 0000000000..17effc3412 --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITUser.m @@ -0,0 +1,106 @@ +#import "BITUser.h" + +/// Data contract class for type User. +@implementation BITUser + +/// +/// Adds all members of this class to a dictionary +/// @returns dictionary to which the members of this class will be added. +/// +- (NSDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary].mutableCopy; + if (self.accountAcquisitionDate != nil) { + [dict setObject:self.accountAcquisitionDate forKey:@"ai.user.accountAcquisitionDate"]; + } + if (self.accountId != nil) { + [dict setObject:self.accountId forKey:@"ai.user.accountId"]; + } + if (self.userAgent != nil) { + [dict setObject:self.userAgent forKey:@"ai.user.userAgent"]; + } + if (self.userId != nil) { + [dict setObject:self.userId forKey:@"ai.user.id"]; + } + if(self.storeRegion != nil) { + [dict setObject:self.storeRegion forKey:@"ai.user.storeRegion"]; + } + if(self.authUserId != nil) { + [dict setObject:self.authUserId forKey:@"ai.user.authUserId"]; + } + if(self.anonUserAcquisitionDate != nil) { + [dict setObject:self.anonUserAcquisitionDate forKey:@"ai.user.anonUserAcquisitionDate"]; + } + if(self.authUserAcquisitionDate != nil) { + [dict setObject:self.authUserAcquisitionDate forKey:@"ai.user.authUserAcquisitionDate"]; + } + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if(self) { + _accountAcquisitionDate = [coder decodeObjectForKey:@"self.accountAcquisitionDate"]; + _accountId = [coder decodeObjectForKey:@"self.accountId"]; + _userAgent = [coder decodeObjectForKey:@"self.userAgent"]; + _userId = [coder decodeObjectForKey:@"self.userId"]; + _storeRegion = [coder decodeObjectForKey:@"self.storeRegion"]; + _authUserId = [coder decodeObjectForKey:@"self.authUserId"]; + _anonUserAcquisitionDate = [coder decodeObjectForKey:@"self.anonUserAcquisitionDate"]; + _authUserAcquisitionDate = [coder decodeObjectForKey:@"self.authUserAcquisitionDate"]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.accountAcquisitionDate forKey:@"self.accountAcquisitionDate"]; + [coder encodeObject:self.accountId forKey:@"self.accountId"]; + [coder encodeObject:self.userAgent forKey:@"self.userAgent"]; + [coder encodeObject:self.userId forKey:@"self.userId"]; + [coder encodeObject:self.storeRegion forKey:@"self.storeRegion"]; + [coder encodeObject:self.authUserId forKey:@"self.authUserId"]; + [coder encodeObject:self.anonUserAcquisitionDate forKey:@"self.anonUserAcquisitionDate"]; + [coder encodeObject:self.authUserAcquisitionDate forKey:@"self.authUserAcquisitionDate"]; +} + +#pragma mark - Compare + +- (BOOL)isEqualToUser:(BITUser *)aUser { + if (aUser == self) { + return YES; + } + if (!aUser || ![aUser isKindOfClass:[self class]]) { + return NO; + } + if (![self.userId isEqualToString: aUser.userId]) { + return NO; + } + if(![self.authUserId isEqualToString: aUser.authUserId]) { + return NO; + } + if (![self.accountId isEqualToString: aUser.accountId]) { + return NO; + } + if(![self.anonUserAcquisitionDate isEqualToString: aUser.anonUserAcquisitionDate]) { + return NO; + } + if(![self.authUserAcquisitionDate isEqualToString: aUser.authUserAcquisitionDate]) { + return NO; + } + if (![self.accountAcquisitionDate isEqualToString: aUser.accountAcquisitionDate]) { + return NO; + } + if (![self.userAgent isEqualToString: aUser.userAgent]) { + return NO; + } + if(![self.storeRegion isEqualToString: aUser.storeRegion]) { + return NO; + } + + return YES; +} + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITWebTableViewCell.h b/submodules/HockeySDK-iOS/Classes/BITWebTableViewCell.h new file mode 100644 index 0000000000..c1706d2fdb --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITWebTableViewCell.h @@ -0,0 +1,44 @@ +/* + * Author: Andreas Linde + * Peter Steinberger + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011-2012 Peter Steinberger. + * 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 +#import + +@interface BITWebTableViewCell : UITableViewCell + +@property (nonatomic, strong) UIWebView *webView; +@property (nonatomic, copy) NSString *webViewContent; +@property (nonatomic, assign) CGSize webViewSize; +@property (nonatomic, strong) UIColor *cellBackgroundColor; + +- (void)addWebView; + +@end diff --git a/submodules/HockeySDK-iOS/Classes/BITWebTableViewCell.m b/submodules/HockeySDK-iOS/Classes/BITWebTableViewCell.m new file mode 100644 index 0000000000..a7dc7c810f --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/BITWebTableViewCell.m @@ -0,0 +1,199 @@ +/* + * Author: Andreas Linde + * Peter Steinberger + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011-2012 Peter Steinberger. + * 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 "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_UPDATES + +#import "BITWebTableViewCell.h" + +@implementation BITWebTableViewCell + +static NSString* BITWebTableViewCellHtmlTemplate = @"\ +\ +\ +\ +\ +\ +\ +%@\ +\ +\ +"; + + +#pragma mark - private + +- (void)addWebView { + if(self.webViewContent) { + CGRect webViewRect = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height); + if(!self.webView) { + self.webView = [[UIWebView alloc] initWithFrame:webViewRect]; + [self addSubview:self.webView]; + self.webView.hidden = YES; + self.webView.backgroundColor = self.cellBackgroundColor; + self.webView.opaque = NO; + self.webView.delegate = self; + self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth; + + for(UIView* subView in self.webView.subviews){ + if([subView isKindOfClass:[UIScrollView class]]){ + // disable scrolling + UIScrollView *sv = (UIScrollView *)subView; + sv.scrollEnabled = NO; + sv.bounces = NO; + + // hide shadow + for (UIView* shadowView in [subView subviews]) { + if ([shadowView isKindOfClass:[UIImageView class]]) { + shadowView.hidden = YES; + } + } + } + } + } + else + self.webView.frame = webViewRect; + + NSString *deviceWidth = [NSString stringWithFormat:@"%.0f", (double)CGRectGetWidth(self.bounds)]; + + //HockeySDKLog(@"%@\n%@\%@", PSWebTableViewCellHtmlTemplate, deviceWidth, self.webViewContent); + NSString *contentHtml = [NSString stringWithFormat:BITWebTableViewCellHtmlTemplate, deviceWidth, self.webViewContent]; + [self.webView loadHTMLString:contentHtml baseURL:[NSURL URLWithString:@"about:blank"]]; + } +} + +- (void)showWebView { + self.webView.hidden = NO; + self.textLabel.text = @""; + [self setNeedsDisplay]; +} + + +- (void)removeWebView { + if(self.webView) { + self.webView.delegate = nil; + [self.webView resignFirstResponder]; + [self.webView removeFromSuperview]; + } + self.webView = nil; + [self setNeedsDisplay]; +} + + +- (void)setWebViewContent:(NSString *)aWebViewContent { + if (_webViewContent != aWebViewContent) { + _webViewContent = aWebViewContent; + + // add basic accessibility (prevents "snarfed from ivar layout") logs + self.accessibilityLabel = aWebViewContent; + } +} + + +#pragma mark - NSObject + +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { + if((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) { + self.cellBackgroundColor = [UIColor clearColor]; + } + return self; +} + +- (void)dealloc { + [self removeWebView]; +} + + +#pragma mark - UIView + +- (void)setFrame:(CGRect)aFrame { + BOOL needChange = !CGRectEqualToRect(aFrame, self.frame); + [super setFrame:aFrame]; + + if (needChange) { + [self addWebView]; + } +} + + +#pragma mark - UITableViewCell + +- (void)prepareForReuse { + [self removeWebView]; + self.webViewContent = nil; + [super prepareForReuse]; +} + + +#pragma mark - UIWebViewDelegate + +- (BOOL)webView:(UIWebView *) __unused webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { + switch (navigationType) { + case UIWebViewNavigationTypeLinkClicked: + [self openURL:request.URL]; + return NO; + case UIWebViewNavigationTypeOther: + return YES; + case UIWebViewNavigationTypeBackForward: + case UIWebViewNavigationTypeFormResubmitted: + case UIWebViewNavigationTypeFormSubmitted: + case UIWebViewNavigationTypeReload: + return NO; + } +} + +- (void)webViewDidFinishLoad:(UIWebView *) __unused webView { + if(self.webViewContent) + [self showWebView]; + + CGRect frame = self.webView.frame; + frame.size.height = 1; + self.webView.frame = frame; + CGSize fittingSize = [self.webView sizeThatFits:CGSizeZero]; + frame.size = fittingSize; + self.webView.frame = frame; + + // sizeThatFits is not reliable - use javascript for optimal height + NSString *output = [self.webView stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight;"]; + self.webViewSize = CGSizeMake(fittingSize.width, [output integerValue]); +} + +#pragma mark - Helper + +- (void)openURL:(NSURL *)URL { + [[UIApplication sharedApplication] openURL:URL]; +} + +@end + +#endif /* HOCKEYSDK_FEATURE_UPDATES */ diff --git a/submodules/HockeySDK-iOS/Classes/HockeySDK.h b/submodules/HockeySDK-iOS/Classes/HockeySDK.h new file mode 100644 index 0000000000..0c0f2c76be --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/HockeySDK.h @@ -0,0 +1,97 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde. + * 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 + + +#if !defined (TARGET_OS_IOS) // Defined starting in iOS 9 +#define TARGET_OS_IOS 1 +#endif + + +#import "HockeySDKFeatureConfig.h" +#import "HockeySDKEnums.h" +#import "HockeySDKNullability.h" +#import "BITAlertAction.h" + +#import "BITHockeyManager.h" +#import "BITHockeyManagerDelegate.h" + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER || HOCKEYSDK_FEATURE_FEEDBACK +#import "BITHockeyAttachment.h" +#endif + +#if HOCKEYSDK_FEATURE_CRASH_REPORTER +#import "BITCrashManager.h" +#import "BITCrashAttachment.h" +#import "BITCrashManagerDelegate.h" +#import "BITCrashDetails.h" +#import "BITCrashMetaData.h" +#endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ + +#if HOCKEYSDK_FEATURE_UPDATES +#import "BITUpdateManager.h" +#import "BITUpdateManagerDelegate.h" +#import "BITUpdateViewController.h" +#endif /* HOCKEYSDK_FEATURE_UPDATES */ + +#if HOCKEYSDK_FEATURE_STORE_UPDATES +#import "BITStoreUpdateManager.h" +#import "BITStoreUpdateManagerDelegate.h" +#endif /* HOCKEYSDK_FEATURE_STORE_UPDATES */ + +#if HOCKEYSDK_FEATURE_FEEDBACK +#import "BITFeedbackManager.h" +#import "BITFeedbackManagerDelegate.h" +#import "BITFeedbackActivity.h" +#import "BITFeedbackComposeViewController.h" +#import "BITFeedbackComposeViewControllerDelegate.h" +#import "BITFeedbackListViewController.h" +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ + +#if HOCKEYSDK_FEATURE_AUTHENTICATOR +#import "BITAuthenticator.h" +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ + +#if HOCKEYSDK_FEATURE_METRICS +#import "BITMetricsManager.h" +#endif /* HOCKEYSDK_FEATURE_METRICS */ + +// Notification message which HockeyManager is listening to, to retry requesting updated from the server. +// This can be used by app developers to trigger additional points where the HockeySDK can try sending +// pending crash reports or feedback messages. +// By default the SDK retries sending pending data only when the app becomes active. +#define BITHockeyNetworkDidBecomeReachableNotification @"BITHockeyNetworkDidBecomeReachable" + +extern NSString *const kBITCrashErrorDomain; +extern NSString *const kBITUpdateErrorDomain; +extern NSString *const kBITFeedbackErrorDomain; +extern NSString *const kBITAuthenticatorErrorDomain; +extern NSString *const __attribute__((unused)) kBITHockeyErrorDomain; + diff --git a/submodules/HockeySDK-iOS/Classes/HockeySDKEnums.h b/submodules/HockeySDK-iOS/Classes/HockeySDKEnums.h new file mode 100644 index 0000000000..9caa3dbcda --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/HockeySDKEnums.h @@ -0,0 +1,187 @@ +// +// HockeySDKEnums.h +// HockeySDK +// +// Created by Lukas Spieß on 08/10/15. +// +// + +#ifndef HockeySDK_HockeyEnums_h +#define HockeySDK_HockeyEnums_h + +/** + * HockeySDK Log Levels + */ +typedef NS_ENUM(NSUInteger, BITLogLevel) { + /** + * Logging is disabled + */ + BITLogLevelNone = 0, + /** + * Only errors will be logged + */ + BITLogLevelError = 1, + /** + * Errors and warnings will be logged + */ + BITLogLevelWarning = 2, + /** + * Debug information will be logged + */ + BITLogLevelDebug = 3, + /** + * Logging will be very chatty + */ + BITLogLevelVerbose = 4 +}; + +typedef NSString *(^BITLogMessageProvider)(void); +typedef void (^BITLogHandler)(BITLogMessageProvider messageProvider, BITLogLevel logLevel, const char *file, const char *function, uint line); + +/** + * HockeySDK App environment + */ +typedef NS_ENUM(NSInteger, BITEnvironment) { + /** + * App has been downloaded from the AppStore + */ + BITEnvironmentAppStore = 0, + /** + * App has been downloaded from TestFlight + */ + BITEnvironmentTestFlight = 1, + /** + * App has been installed by some other mechanism. + * This could be Ad-Hoc, Enterprise, etc. + */ + BITEnvironmentOther = 99 +}; + +/** + * HockeySDK Crash Reporter error domain + */ +typedef NS_ENUM (NSInteger, BITCrashErrorReason) { + /** + * Unknown error + */ + BITCrashErrorUnknown, + /** + * API Server rejected app version + */ + BITCrashAPIAppVersionRejected, + /** + * API Server returned empty response + */ + BITCrashAPIReceivedEmptyResponse, + /** + * Connection error with status code + */ + BITCrashAPIErrorWithStatusCode +}; + +/** + * HockeySDK Update error domain + */ +typedef NS_ENUM (NSInteger, BITUpdateErrorReason) { + /** + * Unknown error + */ + BITUpdateErrorUnknown, + /** + * API Server returned invalid status + */ + BITUpdateAPIServerReturnedInvalidStatus, + /** + * API Server returned invalid data + */ + BITUpdateAPIServerReturnedInvalidData, + /** + * API Server returned empty response + */ + BITUpdateAPIServerReturnedEmptyResponse, + /** + * Authorization secret missing + */ + BITUpdateAPIClientAuthorizationMissingSecret, + /** + * No internet connection + */ + BITUpdateAPIClientCannotCreateConnection +}; + +/** + * HockeySDK Feedback error domain + */ +typedef NS_ENUM(NSInteger, BITFeedbackErrorReason) { + /** + * Unknown error + */ + BITFeedbackErrorUnknown, + /** + * API Server returned invalid status + */ + BITFeedbackAPIServerReturnedInvalidStatus, + /** + * API Server returned invalid data + */ + BITFeedbackAPIServerReturnedInvalidData, + /** + * API Server returned empty response + */ + BITFeedbackAPIServerReturnedEmptyResponse, + /** + * Authorization secret missing + */ + BITFeedbackAPIClientAuthorizationMissingSecret, + /** + * No internet connection + */ + BITFeedbackAPIClientCannotCreateConnection +}; + +/** + * HockeySDK Authenticator error domain + */ +typedef NS_ENUM(NSInteger, BITAuthenticatorReason) { + /** + * Unknown error + */ + BITAuthenticatorErrorUnknown, + /** + * Network error + */ + BITAuthenticatorNetworkError, + + /** + * API Server returned invalid response + */ + BITAuthenticatorAPIServerReturnedInvalidResponse, + /** + * Not Authorized + */ + BITAuthenticatorNotAuthorized, + /** + * Unknown Application ID (configuration error) + */ + BITAuthenticatorUnknownApplicationID, + /** + * Authorization secret missing + */ + BITAuthenticatorAuthorizationSecretMissing, + /** + * Not yet identified + */ + BITAuthenticatorNotIdentified, +}; + +/** + * HockeySDK global error domain + */ +typedef NS_ENUM(NSInteger, BITHockeyErrorReason) { + /** + * Unknown error + */ + BITHockeyErrorUnknown +}; + +#endif /* HockeySDK_HockeyEnums_h */ diff --git a/submodules/HockeySDK-iOS/Classes/HockeySDKFeatureConfig.h b/submodules/HockeySDK-iOS/Classes/HockeySDKFeatureConfig.h new file mode 100644 index 0000000000..6808e6721c --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/HockeySDKFeatureConfig.h @@ -0,0 +1,97 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2013-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. + */ + +/** + * This is the template feature config that is used for debug builds and during development. + * For the Distribution target, we are using separate configs that will be copied over in our build script. + */ + + +#ifndef HockeySDK_HockeySDKFeatureConfig_h +#define HockeySDK_HockeySDKFeatureConfig_h + +/** + * If true, include support for handling crash reports + * + * _Default_: Enabled + */ +#ifndef HOCKEYSDK_FEATURE_CRASH_REPORTER +# define HOCKEYSDK_FEATURE_CRASH_REPORTER 1 +#endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ + + +/** + * If true, include support for managing user feedback + * + * _Default_: Enabled + */ +#ifndef HOCKEYSDK_FEATURE_FEEDBACK +# define HOCKEYSDK_FEATURE_FEEDBACK 0 +#endif /* HOCKEYSDK_FEATURE_FEEDBACK */ + + +/** + * If true, include support for informing the user about new updates pending in the App Store + * + * _Default_: Enabled + */ +#ifndef HOCKEYSDK_FEATURE_STORE_UPDATES +# define HOCKEYSDK_FEATURE_STORE_UPDATES 0 +#endif /* HOCKEYSDK_FEATURE_STORE_UPDATES */ + + +/** + * If true, include support for authentication installations for Ad-Hoc and Enterprise builds + * + * _Default_: Enabled + */ +#ifndef HOCKEYSDK_FEATURE_AUTHENTICATOR +# define HOCKEYSDK_FEATURE_AUTHENTICATOR 1 +#endif /* HOCKEYSDK_FEATURE_AUTHENTICATOR */ + + +/** + * If true, include support for handling in-app updates for Ad-Hoc and Enterprise builds + * + * _Default_: Enabled + */ +#ifndef HOCKEYSDK_FEATURE_UPDATES +# define HOCKEYSDK_FEATURE_UPDATES 1 +#endif /* HOCKEYSDK_FEATURE_UPDATES */ + + +/** + * If true, include support for auto collecting metrics data such as sessions and user + * + * _Default_: Enabled + */ +#ifndef HOCKEYSDK_FEATURE_METRICS +# define HOCKEYSDK_FEATURE_METRICS 0 +#endif /* HOCKEYSDK_FEATURE_METRICS */ + +#endif /* HockeySDK_HockeySDKFeatureConfig_h */ diff --git a/submodules/HockeySDK-iOS/Classes/HockeySDKNullability.h b/submodules/HockeySDK-iOS/Classes/HockeySDKNullability.h new file mode 100644 index 0000000000..f98032296a --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/HockeySDKNullability.h @@ -0,0 +1,35 @@ +// +// HockeyNullability.h +// HockeySDK +// +// Created by Andreas Linde on 12/06/15. +// +// + +#ifndef HockeySDK_HockeyNullability_h +#define HockeySDK_HockeyNullability_h + +// Define nullability fallback for backwards compatibility +#if !__has_feature(nullability) +#define NS_ASSUME_NONNULL_BEGIN +#define NS_ASSUME_NONNULL_END +#define nullable +#define nonnull +#define null_unspecified +#define null_resettable +#define _Nullable +#define _Nonnull +#define __nullable +#define __nonnull +#define __null_unspecified +#endif + +// Fallback for convenience syntax which might not be available in older SDKs +#ifndef NS_ASSUME_NONNULL_BEGIN +#define NS_ASSUME_NONNULL_BEGIN _Pragma("clang assume_nonnull begin") +#endif +#ifndef NS_ASSUME_NONNULL_END +#define NS_ASSUME_NONNULL_END _Pragma("clang assume_nonnull end") +#endif + +#endif /* HockeySDK_HockeyNullability_h */ diff --git a/submodules/HockeySDK-iOS/Classes/HockeySDKPrivate.h b/submodules/HockeySDK-iOS/Classes/HockeySDKPrivate.h new file mode 100644 index 0000000000..800c6f71ae --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/HockeySDKPrivate.h @@ -0,0 +1,97 @@ +/* + * Author: Andreas Linde + * Kent Sutherland + * + * Copyright (c) 2012-2013 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde & Kent Sutherland. + * 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 +#import "BITHockeyLogger.h" + +#ifndef HockeySDK_HockeySDKPrivate_h +#define HockeySDK_HockeySDKPrivate_h + +#define BITHOCKEY_NAME @"HockeySDK" +#define BITHOCKEY_IDENTIFIER @"net.hockeyapp.sdk.ios" +#define BITHOCKEY_CRASH_SETTINGS @"BITCrashManager.plist" +#define BITHOCKEY_CRASH_ANALYZER @"BITCrashManager.analyzer" + +#define BITHOCKEY_FEEDBACK_SETTINGS @"BITFeedbackManager.plist" + +#define BITHOCKEY_USAGE_DATA @"BITUpdateManager.plist" + +#define kBITHockeyMetaUserName @"BITHockeyMetaUserName" +#define kBITHockeyMetaUserEmail @"BITHockeyMetaUserEmail" +#define kBITHockeyMetaUserID @"BITHockeyMetaUserID" + +#define kBITUpdateInstalledUUID @"BITUpdateInstalledUUID" +#define kBITUpdateInstalledVersionID @"BITUpdateInstalledVersionID" +#define kBITUpdateCurrentCompanyName @"BITUpdateCurrentCompanyName" +#define kBITUpdateArrayOfLastCheck @"BITUpdateArrayOfLastCheck" +#define kBITUpdateDateOfLastCheck @"BITUpdateDateOfLastCheck" +#define kBITUpdateDateOfVersionInstallation @"BITUpdateDateOfVersionInstallation" +#define kBITUpdateUsageTimeOfCurrentVersion @"BITUpdateUsageTimeOfCurrentVersion" +#define kBITUpdateUsageTimeForUUID @"BITUpdateUsageTimeForUUID" +#define kBITUpdateInstallationIdentification @"BITUpdateInstallationIdentification" + +#define kBITStoreUpdateDateOfLastCheck @"BITStoreUpdateDateOfLastCheck" +#define kBITStoreUpdateLastStoreVersion @"BITStoreUpdateLastStoreVersion" +#define kBITStoreUpdateLastUUID @"BITStoreUpdateLastUUID" +#define kBITStoreUpdateIgnoreVersion @"BITStoreUpdateIgnoredVersion" + +#define BITHOCKEY_INTEGRATIONFLOW_TIMESTAMP @"BITIntegrationFlowStartTimestamp" + +#define BITHOCKEYSDK_BUNDLE @"HockeySDKResources.bundle" +#define BITHOCKEYSDK_URL @"https://sdk.hockeyapp.net/" + +#define BIT_RGBCOLOR(r,g,b) [UIColor colorWithRed:(CGFloat)((r)/255.0) green:(CGFloat)((g)/255.0) blue:(CGFloat)((b)/255.0) alpha:(CGFloat)1] + +NSBundle *BITHockeyBundle(void); +NSString *BITHockeyLocalizedString(NSString *stringToken); +NSString *BITHockeyMD5(NSString *str); + +#ifndef __IPHONE_11_0 +#define __IPHONE_11_0 110000 +#endif + +#ifndef TARGET_OS_SIMULATOR + + #ifdef TARGET_IPHONE_SIMULATOR + + #define TARGET_OS_SIMULATOR TARGET_IPHONE_SIMULATOR + + #else + + #define TARGET_OS_SIMULATOR 0 + + #endif /* TARGET_IPHONE_SIMULATOR */ + +#endif /* TARGET_OS_SIMULATOR */ + +#define kBITButtonTypeSystem UIButtonTypeSystem + +#endif /* HockeySDK_HockeySDKPrivate_h */ diff --git a/submodules/HockeySDK-iOS/Classes/HockeySDKPrivate.m b/submodules/HockeySDK-iOS/Classes/HockeySDKPrivate.m new file mode 100644 index 0000000000..9cc90e5b2d --- /dev/null +++ b/submodules/HockeySDK-iOS/Classes/HockeySDKPrivate.m @@ -0,0 +1,83 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2011 Andreas Linde. + * 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 "HockeySDK.h" +#import "HockeySDKPrivate.h" +#include + +NSString *const kBITCrashErrorDomain = @"BITCrashReporterErrorDomain"; +NSString *const kBITUpdateErrorDomain = @"BITUpdaterErrorDomain"; +NSString *const kBITFeedbackErrorDomain = @"BITFeedbackErrorDomain"; +NSString *const kBITHockeyErrorDomain = @"BITHockeyErrorDomain"; +NSString *const kBITAuthenticatorErrorDomain = @"BITAuthenticatorErrorDomain"; + +// Load the framework bundle. +NSBundle *BITHockeyBundle(void) { + static NSBundle *bundle = nil; + static dispatch_once_t predicate; + dispatch_once(&predicate, ^{ + NSString* mainBundlePath = [[NSBundle bundleForClass:[BITHockeyManager class]] resourcePath]; + NSString* frameworkBundlePath = [mainBundlePath stringByAppendingPathComponent:BITHOCKEYSDK_BUNDLE]; + bundle = [NSBundle bundleWithPath:frameworkBundlePath]; + }); + return bundle; +} + +NSString *BITHockeyLocalizedString(NSString *stringToken) { + if (!stringToken) return @""; + + NSString *appSpecificLocalizationString = NSLocalizedString(stringToken, @""); + if (appSpecificLocalizationString && ![stringToken isEqualToString:appSpecificLocalizationString]) { + return appSpecificLocalizationString; + } else if (BITHockeyBundle()) { + NSString *bundleSpecificLocalizationString = NSLocalizedStringFromTableInBundle(stringToken, @"HockeySDK", BITHockeyBundle(), @""); + if (bundleSpecificLocalizationString) + return bundleSpecificLocalizationString; + return stringToken; + } else { + return stringToken; + } +} + +NSString *BITHockeyMD5(NSString *str) { + NSData *utf8Bytes = [str dataUsingEncoding:NSUTF8StringEncoding]; + unsigned char result[CC_MD5_DIGEST_LENGTH] = {0}; + CC_MD5( utf8Bytes.bytes, (CC_LONG)utf8Bytes.length, result ); + return [NSString + stringWithFormat: @"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", + result[0], result[1], + result[2], result[3], + result[4], result[5], + result[6], result[7], + result[8], result[9], + result[10], result[11], + result[12], result[13], + result[14], result[15] + ]; +} diff --git a/submodules/HockeySDK-iOS/Documentation/Guides/App Versioning.md b/submodules/HockeySDK-iOS/Documentation/Guides/App Versioning.md new file mode 100644 index 0000000000..f404d6a834 --- /dev/null +++ b/submodules/HockeySDK-iOS/Documentation/Guides/App Versioning.md @@ -0,0 +1,45 @@ +## Introduction + +We suggest to handle beta and release versions in two separate *apps* on HockeyApp with their own bundle identifier (e.g. by adding "beta" to the bundle identifier), so + +* both apps can run on the same device or computer at the same time without interfering, +* release versions do not appear on the beta download pages, and +* easier analysis of crash reports and user feedback. + +We propose the following method to set version numbers in your beta versions: + +* Use both `Bundle Version` and `Bundle Version String, short` in your Info.plist. +* "Bundle Version" should contain a sequential build number, e.g. 1, 2, 3. +* "Bundle Version String, short" should contain the target official version number, e.g. 1.0. + +## HowTo + +The recommended way to do versioning of your app versions is as follows: + +- Each version gets an ongoing `build` number which increases by `1` for every version as `CFBundleVersion` in `Info.plist` +- Additionally `CFBundleShortVersionString` in `Info.plist` will contain you target public version number as a string like `1.0.0` + +This ensures that each app version is uniquely identifiable, and that live and beta version numbers never ever collide. + +This is how to set it up with Xcode 4: + +1. Pick `File | New`, choose `Other` and `Configuration Settings File`, this gets you a new .xcconfig file. +2. Name it `buildnumber.xcconfig` +3. Add one line with this content: `BUILD_NUMBER = 1` +4. Then click on the project on the upper left in the file browser (the same place where you get to build settings), click on the project again in the second-to-left panel, and click on the Info tab at the top of the inner panel. +5. There, you can choose `Based on Configuration File` for each of your targets for each of your configurations (debug, release, etc.) +6. Select your target +7. Select the `Summary` tab +8. For `Build` enter the value: `${BUILD_NUMBER}` +9. Select the `Build Phases` tab +10. Select `Add Build Phase` +11. Choose `Add Run Script` +12. Add the following content: + + if [ "$CONFIGURATION" == "AdHoc_Distribution" ] + then /usr/bin/perl -pe 's/(BUILD_NUMBER = )(\d+)/$1.($2+1)/eg' -i buildnumber.xcconfig + fi +13. Change `AdHoc_Distribution` to the actual name of the Xcode configuration(s) you wnat the build number to be increased. + + *Note:* Configuration names should not contain spaces! +14. If you want to increase the build number before the build actuallry starts, just drag it up \ No newline at end of file diff --git a/submodules/HockeySDK-iOS/Documentation/Guides/Changelog.md b/submodules/HockeySDK-iOS/Documentation/Guides/Changelog.md new file mode 100644 index 0000000000..64aa7af548 --- /dev/null +++ b/submodules/HockeySDK-iOS/Documentation/Guides/Changelog.md @@ -0,0 +1,1072 @@ +## 5.1.2 + +- [IMPROVEMENT] This release uses Xcode 9.2 to compile the SDK. [#502](https://github.com/bitstadium/HockeySDK-iOS/pull/503) +- [BUGFIX] Fix warnings when integrating the SDK as source in Xcode 9. [#501](https://github.com/bitstadium/HockeySDK-iOS/pull/501) +- [BUGFIX] Fix a potential memory leak in `BITChannel`. [#500](https://github.com/bitstadium/HockeySDK-iOS/pull/500) +- [BUGFIX] Version 5.1.X broke support for app extension. We're sorry about this and we've updated our test matrix to make sure this does not happen again. [#499](https://github.com/bitstadium/HockeySDK-iOS/pull/499) +- [BUGFIX] Fix a bug in the Feedback UI when Feedback was shown in landscape. [#498](https://github.com/bitstadium/HockeySDK-iOS/pull/498) + +## 5.1.1 + +- [BUGFIX] Fixes a critical bug that would cause apps to freeze when calling `trackEvent` in UIApplicationDelegate callbacks. [#492](https://github.com/bitstadium/HockeySDK-iOS/pull/493) +- [BUGFIX] Fix a critical bug in the crashonly variant of the SDK. [#49](https://github.com/bitstadium/HockeySDK-iOS/pull/494) + +## 5.1.0 + +- [FEATURE] Add Turkish localization thanks to [Ozgur](https://github.com/ozgur).[#478](https://github.com/bitstadium/HockeySDK-iOS/pull/478) +- [FEATURE] Add support to detect low memory and OS kill heuristics for extensions. Thx to [Dave Weston](https://github.com/dtweston) for this! [#470](https://github.com/bitstadium/HockeySDK-iOS/pull/470) +- [IMPROVEMENT] Support tracking events in the background. [#475](https://github.com/bitstadium/HockeySDK-iOS/pull/475) +- [FIX] Improvements around thread-safety and concurrency for Metrics. [#471](https://github.com/bitstadium/HockeySDK-iOS/pull/471) [#479](https://github.com/bitstadium/HockeySDK-iOS/pull/479) +- [FIX] Fix runtime warnings of Xcode 9's main thread checker tool. [#484](https://github.com/bitstadium/HockeySDK-iOS/pull/484) +- [FIX] Fix caching of previews for attachments to Feedback. [#487](https://github.com/bitstadium/HockeySDK-iOS/pull/487) + +## 5.0.0 + +- [IMPROVEMENT] Use `UIAlertController` in Feedback instead of `UIAlertView`. [#460](https://github.com/bitstadium/HockeySDK-iOS/pull/460) +- [BUGFIX] Fix bugs in the Feedback UI on iOS 11. [#459](https://github.com/bitstadium/HockeySDK-iOS/pull/459) + +## 5.0.0-beta.2 + +- [FEATURE] Added support for Metrics in app extensions. [#449](https://github.com/bitstadium/HockeySDK-iOS/pull/449) +- [FEATURE] User Metrics can now be enabled after it was disabled [#451)(https://github.com/bitstadium/HockeySDK-iOS/pull/451) +- [IMPROVEMENT] Don't use `UIAlertView` but `UIAlertController`.[#446](https://github.com/bitstadium/HockeySDK-iOS/pull/446) +- [IMPROVEMENT] `BITAttributedLabel` is now based on `TTTAttributedLabel` 2.0. [#450](https://github.com/bitstadium/HockeySDK-iOS/pull/450) +- [BUGFIX] Fix a bug in `BITAuthenticator`. [#447](https://github.com/bitstadium/HockeySDK-iOS/pull/447) +- [BUGFIX] Fix for a bug in `BITImageAnnotation`. [#453](https://github.com/bitstadium/HockeySDK-iOS/pull/453) + +## 5.0.0-beta.1 + +This version drops support for iOS 7. There is not other breaking change at this point. + +- [IMPROVEMENT] The code has been cleaned up as we have decided to drop support for iOS 7. +- [IMPROVEMENT] `properties` of type `NSString` now use the `copy` attribute. +- [BUGFIX] The logic that makes sure that the for HockeySDK-iOS is excluded from backups was changed to make sure it doesn't block app launch [#443](https://github.com/bitstadium/HockeySDK-iOS/pull/443). + +## 4.1.6 + +- [BUGFIX] Fixed a string in the Italian translation [#430](https://github.com/bitstadium/HockeySDK-iOS/pull/430). +- [BUGFIX] Fix bug that prevented images that were attached to Feedback from loading [#428](https://github.com/bitstadium/HockeySDK-iOS/pull/428) – thx to [Zoltan](https://github.com/Xperion-meszaroz) for the contribution. +- [IMPROVEMENT] Improved the accessibility of the Feedback UI, it now uses the proper accessibility traits. +- [IMPROVEMENT] The crash-only flavor of the SDK now doesn't contain the Metrics feature. +- [IMPROVEMENT] The SDK can be compiled using Xcode 9 [#423](https://github.com/bitstadium/HockeySDK-iOS/pull/423) – thx to [Stephan](https://github.com/diederich) and [Piet](https://github.com/pietbrauer) for the contribution! +- [IMPROVEMENT] Metrics info will be send to the backend every time the application goes from foreground to background [#429](https://github.com/bitstadium/HockeySDK-iOS/pull/429) - thx to [Ivan](https://github.com/MatkovIvan) for the contribution. +- [IMPROVEMENT] Our README.md now use gender-neutral language [#427](https://github.com/bitstadium/HockeySDK-iOS/pull/427). + +## 4.1.5 + +This release officially drops the support for iOS 6. + +- [BUGFIX] Remove the dependency for AssetLibrary for the default subspec in our podspec [#403](https://github.com/bitstadium/HockeySDK-iOS/pull/403). Thanks to [@tschob](https://github.com/tschob) for the pointer. +- [BUGFIX] A couple of visual bugs have been fixed [#404](https://github.com/bitstadium/HockeySDK-iOS/pull/404) thanks to [@dweston](https://github.com/dtweston). +- [IMPROVEMENT] We have improved accessibility of our feedback UI [#409](https://github.com/bitstadium/HockeySDK-iOS/pull/409) with help by [@erychagov](https://github.com/erychagov). + +## 4.1.4 + +- [IMPROVEMENT] Test targets won't be build in the run phase of the framework, which makes it possible to build individual configurations when using Carthage. Thanks a lot @wiedem for your contribution! [394](https://github.com/bitstadium/HockeySDK-iOS/pull/394) +- [IMPROVEMENT] We've reverted to a build based on PLCrashReporter 1.2.1 as 1.3 comes with unintended namespace collisions in some edge cases that result in worse crash reporting than you were used to. +- [BUGFIX] Fixes a crash on iOS 9 when attaching data to feedback [#395](https://github.com/bitstadium/HockeySDK-iOS/issues/395) +- [BUGFIX] Disabling the `BITFeedbackManager` now disables the various `BITFeedbackObservationModes`. [#390](https://github.com/bitstadium/HockeySDK-iOS/pull/390) + +## 4.1.3 + +- [NEW] Added `forceNewFeedbackThreadForFeedbackManager:`-callback to `BITFeedbackManagerDelegate` to force a new feedback thread for each new feedback. +- [NEW] Norwegian (Bokmal) localization +- [NEW] Persian (Farsi) localization +- [BUGFIX] Fix analyzer warning in `BITChannelManagerTests` +- [BUGFIX] Add check for nil in `BITChannel`. + +## 4.1.2 + +- [NEW] New `shouldDisplayUpdateAlertForUpdateManager`-API [#339](https://github.com/bitstadium/HockeySDK-iOS/pull/339) to make the moment of appearance for custom update UI even more customizable. +- [IMPROVEMENT] Fix static analyzer warnings. [#351](https://github.com/bitstadium/HockeySDK-iOS/pull/351) +- [IMPROVEMENT] Internal structure of embedded frameworks changed [#352](https://github.com/bitstadium/HockeySDK-iOS/pull/352) +- [IMPROVEMENT] Upgrade to PLCrashReporter 1.3 +- [BUGFIX] Enable bitcode in all configurations [#344](https://github.com/bitstadium/HockeySDK-iOS/pull/344) +- [BUGFIX] Fixed anonymisation of binary paths when running in the simulator [#347](https://github.com/bitstadium/HockeySDK-iOS/pull/347) +- - [BUGFIX] Rename configurations to not break Carthage integration [#353](https://github.com/bitstadium/HockeySDK-iOS/pull/353) + +## 4.1.1 + +**Attention** Due to changes in iOS 10, it is now necessary to include the `NSPhotoLibraryUsageDescription` in your app's Info.plist file if you want to use HockeySDK's Feedback feature. Since using the feature without the plist key present could lead to an App Store rejection, our default CocoaPods configuration does not include the Feedback feature anymore. +If you want to continue to use it, use this in your `Podfile`: + +```ruby +pod "HockeySDK", :subspecs => ['AllFeaturesLib'] +``` + +Additionally, we now also provide a new flavor in our binary distribution. To use all features, including Feedback, use `HockeySDK.embeddedframework` from the `HockeySDKAllFeatures` folder. + +- [NEW] The property `userDescription` on `BITCrashMetaData` had to be renamed to `userProvidedeDescription` to provide a name clash with Apple Private API +- [IMPROVEMENT] Warn if the Feedback feature is being used without `NSPhotoLibraryUsageDescription` being present +- [IMPROVEMENT] Updated Chinese translations +- [IMPROVEMENT] Set web view baseURL to `about:blank` to improve security +- [BUGFIX] Fix an issue in the telemetry channel that could be triggered in multi-threaded environments +- [BUGFIX] Fix several small layout issues by updating to a newer version of TTTAttributedLabel +- [BUGFIX] Fix app icons with unusual filenames not showing in the in-app update prompt + +## 4.1.0 + +- Includes improvements from 4.0.2 release of the SDK. +- [NEW] Additional API to track an event with properties and measurements. + +## 4.1.0-beta.2 + +- [BUGFIX] Fixes an issue where the whole app's Application Support directory was accidentally excluded from backups. +This SDK release explicitly includes the Application Support directory into backups. If you want to opt-out of this fix and keep the Application Directory's backup flag untouched, add the following line above the SDK setup code: + + - Objective-C: + ```objc + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"BITExcludeApplicationSupportFromBackup"]; + ``` + + - Swift: + ```swift + NSUserDefaults.standardUserDefaults().setBool(true, forKey: "BITExcludeApplicationSupportFromBackup") + ``` + +- [NEW] Add more fine-grained log levels +- [NEW] Add ability to connect existing logging framework +- [BUGFIX] Make CrashManager property `serverURL` individual setable +- [BUGFIX] Properly dispatch `dismissViewController` call to main queue +- [BUGFIX] Fixes an issue that prevented preparedItemsForFeedbackManager: delegate method from working + +## Version 4.1.0-beta.1 + +- [IMPROVEMENT] Prevent User Metrics from being sent if `BITMetricsManager` has been disabled. + +## Version 4.1.0-alpha.2 + +- [BUGFIX] Fix different bugs in the events sending pipeline + +## Version 4.1.0-alpha.1 + +- [NEW] Add ability to track custom events +- [IMPROVEMENT] Events are always persisted, even if the app crashes +- [IMPROVEMENT] Allow disabling `BITMetricsManager` at any time +- [BUGFIX] Server URL is now properly customizable +- [BUGFIX] Fix memory leak in networking code +- [IMPROVEMENT] Optimize tests and always build test target +- [IMPROVEMENT] Reuse `NSURLSession` object +- [IMPROVEMENT] Under the hood improvements and cleanup + +## Version 4.0.2 + +- [BUGFIX] Add Bitcode marker back to simulator slices. This is necessary because otherwise `lipo` apparently strips the Bitcode sections from the merged library completely. As a side effect, this unfortunately breaks compatibility with Xcode 6. [#310](https://github.com/bitstadium/HockeySDK-iOS/pull/310) +- [IMPROVEMENT] Improve error detection and logging during crash processing in case the app is sent to the background while crash processing hasn't finished.[#311](https://github.com/bitstadium/HockeySDK-iOS/pull/311) + +## Version 4.0.1 + +- [BUGFIX] Fixes an issue where the whole app's Application Support directory was accidentally excluded from backups. +This SDK release explicitly includes the Application Support directory into backups. If you want to opt-out of this fix and keep the Application Directory's backup flag untouched, add the following line above the SDK setup code: + + - Objective-C: + ```objc + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"kBITExcludeApplicationSupportFromBackup"]; + ``` + + - Swift: + ```swift + NSUserDefaults.standardUserDefaults().setBool(true, forKey: "kBITExcludeApplicationSupportFromBackup") + ``` + +- [BUGFIX] Fixes an issue that prevented preparedItemsForFeedbackManager: delegate method from working + +## Version 4.0.0 + +- [NEW] Added official Carthage support +- [NEW] Added `preparedItemsForFeedbackManager:` method in `BITFeedbackManagerDelegate` to allow to provide items with every possible method of showing the feedback compose dialog. +- [UPDATE] Our CrashOnly binary now includes User Metrics which enables crash free users statistics +- [UPDATE] Deprecate `feedbackComposerPreparedItems` property in favor of the new delegate method. +- [IMPROVEMENT] Prefix GZIP category on NSData to prevent symbol collisions +- [BUGFIX] Add minor UI bug when adding arrow annotation to feedback image + +## Version 4.0.0-beta.1 + +- [NEW] User Metrics including users and sessions data is now in public beta + +## Version 4.0.0-alpha.2 + +- [UPDATE] Include changes from HockeySDK 3.8.6 + +## Version 4.0.0-alpha.1 + +- [NEW] Added `BITMetricsManager` to track users and sessions +- [UPDATE] Remove previously deprecated UpdateManagerDelegate method `-viewControllerForUpdateManager:` +- [UPDATE] Remove previously deprecated CrashManagerDelegate methods `-userNameForCrashManager:` and `-userEmailForCrashManager:` +- [UPDATE] Remove previously deprecated property `appStoreEnvironment` +- [UPDATE] Remove previously deprecated misspelled `timeintervalCrashInLastSessionOccured` property +- [UPDATE] Remove previously deprecated misspelled `BITFeedbackListViewCellPresentatationStyle` enum + +## Version 3.8.6 + +- [UPDATE] Some minor refactoring +- [BUGFIX] Fix crash in `BITCrashReportTextFormatter` in cases where processPath is unexpectedly nil +- [BUGFIX] Fix bug where feedback image could only be added once +- [BUGFIX] Fix URL encoding bug in BITUpdateManager +- [BUGFIX] Include username, email, etc. in `appNotTerminatingCleanly` reports +- [BUGFIX] Fix NSURLSession memory leak in Swift apps +- [BUGFIX] Fix issue preventing attachment from being included when sending non-clean termination report +- [IMPROVEMENT] Anonymize binary path in crash report +- [IMPROVEMENT] Support escaping of additional characters (URL encoding) +- [IMPROVEMENT] Support Bundle Identifiers which contain whitespaces + +## Version 3.8.5 + +- [UPDATE] Some minor improvements to our documentation +- [BUGFIX] Fix a crash where `appStoreReceiptURL` was accidentally accessed on iOS 6 +- [BUGFIX] Fix a warning when implementing `BITHockeyManagerDelegate` + +## Version 3.8.4 + +- [BUGFIX] Fix a missing header in the `HockeySDK.h` umbrella +- [BUGFIX] Fix several type comparison warnings + +## Version 3.8.3 + +- [NEW] Adds new `appEnvironment` property to indicate the environment the app is running in. This replaces the old `isAppStoreEnvironment` which is now deprecated. We can now differentiate between apps installed via TestFlight or the AppStore +- [NEW] Distributed zip file now also contains our documentation +- [UPDATE] Prevent issues with duplicate symbols from PLCrashReporter +- [UPDATE] Remove several typos in our documentation and improve instructions for use in extensions +- [UPDATE] Add additional nil-checks before calling blocks +- [UPDATE] Minor code readability improvements +- [BUGFIX] `BITFeedbackManager`: Fix Feedback Annotations not working on iPhones running iOS 9 +- [BUGFIX] Switch back to using UIAlertView to prevent several issues. We will add a more robust solution which uses UIAlertController in a future update. +- [BUGFIX] Fix several small issues in our CrashOnly builds +- [BUGFIX] Minor fixes for memory leaks +- [BUGFIX] Fix crashes because completion blocks were not properly dispatched on the main thread + +## Version 3.8.2 + +- [UPDATE] Added support for Xcode 6.x +- [UPDATE] Requires iOS 7 or later as base SDK, deployment target iOS 6 or later +- [UPDATE] Updated PLCrashReporter build to exclude Bitcode in Simulator slices + +## Version 3.8.1 + +- [UPDATE] Updated PLCrashReporter build using Xcode 7 (7A220) + +## Version 3.8 + +- [NEW] Added Bitcode support +- [UPDATE] Requires Xcode 7 or later +- [UPDATE] Requires iOS 9 or later as base SDK, deployment target iOS 6 or later +- [UPDATE] Updated PLCrashReporter build using Xcode 7 +- [UPDATE] Use `UIAlertController` when available +- [UPDATE] Added full support for `NSURLSession` +- [UPDATE] Removed statusbar adjustment code (which isn't needed any longer) +- [UPDATE] Removed kBITTextLabel... defines and use NSText.. instead +- [UPDATE] Removed a few `#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1` since iOS 7 or later is now required as base SDK +- [BUGFIX] `BITFeedbackManager`: Fixed feedback compose view rotation issue +- [BUGFIX] `BITFeedbackManager`: Fixed `Add Image` button not always presented centered +- [BUGFIX] Additional minor fixes + +## Version 3.8-RC.1 + +- [UPDATE] Added full support for `NSURLSession` +- [BUGFIX] `BITFeedbackManager`: Fixed feedback compose view rotation issue +- [BUGFIX] `BITFeedbackManager`: Fixed `Add Image` button not always presented centered +- [BUGFIX] Additional minor fixes + +## Version 3.8-Beta.1 + +- [NEW] Added Bitcode support +- [UPDATE] Requires Xcode 7 or later +- [UPDATE] Requires iOS 7 or later as base SDK +- [UPDATE] Silenced deprecation warnings for `NSURLConnection` calls, these will be refactored in a future update +- [UPDATE] Removed statusbar adjustment code (which isn't needed any longer) +- [UPDATE] Removed kBITTextLabel... defines and use NSText.. instead +- [UPDATE] Removed a few `#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1` since iOS 7 or later is now required as base SDK +- [UPDATE] Use `UIAlertController` when available + +## Version 3.7.3 + +- [BUGFIX] `BITCrashManager`: Updated PLCrashReporter build created with Xcode 6.4 to solve a duplicate symbol error some users are experiencing +- [BUGFIX] `BITUpdateManager`: Fixed updating an app not triggering a crash report if `enableAppNotTerminatingCleanlyDetection` is enabled +- [BUGFIX] Additional minor fixes + +## Version 3.7.2 + +- [BUGFIX] `BITCrashManager`: Added workaround for a bug observed in iOS 9 beta's dyld triggering an infinite loop on startup +- [BUGFIX] `BITFeedbackManager`: Fixed a crash in the feedback UI that can occur when rotating the device while data is being loaded +- [BUGFIX] Fixed `Info.plist` entries in `HockeySDKResources.bundle` which cause Xcode 7 to show an error when uploading an app to iTunes Connect +- [BUGFIX] Additional minor fixes + +## Version 3.7.1 + +- [BUGFIX] `CocoaPods`: Fixes the default podspec with binary distribution +- [BUGFIX] `CocoaPods`: Changes `HockeySDK-Source` to use non configurable approach, since we couldn't make it work reliably in all scenarios + +## Version 3.7.0 + +- [NEW] Simplified installation process. If support for modules is enabled in the target project (default for most projects), it’s no longer necessary to add the frameworks manually +- [NEW] `CocoaPods`: Default pod uses binary distribution and offers crash only build as a subspec +- [NEW] `CocoaPods`: New `HockeySDK-Source` pod integrates via source code and offers feature set customization via subspecs. Note: We do not support building with Xcode 7 yet! +- [NEW] `BITCrashManager`: Added support for unhandled C++ exceptions (requires to link `libc++`) +- [NEW] `BITCrashManager`: Sending crash reports via `NSURLSession` whenever possible +- [NEW] `BITCrashManager`: Added process ID to `BITCrashDetails` +- [NEW] `BITCrashManager`: Added `CFBundleShortVersionString` value to crash reports +- [NEW] `BITFeedbackManager`: "Add Image" button in feedback compose view can now be hidden using `feedbackComposeHideImageAttachmentButton` property +- [NEW] `BITFeedbackManagerDelegate`: Added `allowAutomaticFetchingForNewFeedbackForManager:` to define if the SDK should fetch new messages on app startup and when the app is coming into foreground. +- [NEW] Added disableInstallTracking property to disable installation tracking (AppStore only). +- [UPDATE] Restructured installation documentation +- [BUGFIX] `BITCrashManager`: Fixed offline issue showing crash alert over and over again with unsent crash reports +- [BUGFIX] `BITFeedbackManager`: Improved screenshot handling on slow devices +- [BUGFIX] `BITStoreUpdateManager`: Delegate property wasn't propagated correctly +- [BUGFIX] Fixed various compiler warnings & other improvements + +## Version 3.6.4 + +- [BUGFIX] Fixed a build issue + +## Version 3.6.3 + +- [NEW] `BITCrashManager`: Added launch time to crash reports +- [NEW] `BITFeedbackManager`: Added support for setting tintColor for feedback list buttons +- [NEW] `BITFeedbackManager`: Added `feedbackComposerPreparedItems` to pre-fill feedback compose UI message with given items +- [NEW] `BITUpdateManagerDelegate`: Added `willStartDownloadAndUpdate` to be notified before beta update starts +- [UPDATE] Improved CocoaPods support to allow building as a native iOS 8 framework +- [UPDATE] Keychain is now accessed with `kSecAttrAccessibleAlwaysThisDeviceOnly` to support apps that are running in the background and the device is still locked +- [UPDATE] Reduced file size of images in `HockeySDKResources.bundle` by 63% +- [UPDATE] `BITCrashManager`: `timeintervalCrashInLastSessionOccured` property is deprecated due to typo, use `timeIntervalCrashInLastSessionOccurred` instead +- [UPDATE] `BITFeedbackManager`: `BITFeedbackListViewCellPresentatationStyle` is deprecated due to a typo, use `BITFeedbackListViewCellPresentationStyle` instead +- [UPDATE] `BITAuthenticator`: Use NSLog instead of an UIAlertView in case of keychain issues +- [BUGFIX] `BITCrashManager`: Fixed issue with `appNotTerminatingCleanlyDetection` for some scenarios +- [BUGFIX] `BITFeedbackManager`: Fixed a crash when deleting feedback attachments +- [BUGFIX] `BITFeedbackManager`: Fixed a crash related to viewing attachments +- [BUGFIX] `BITFeedbackManager`: Fixed landscape screenshot issues in iOS 8 +- [BUGFIX] `BITFeedbackManager`: Fixed various issues in feedback compose UI +- [BUGFIX] `BITFeedbackManager`: Fixed loading issues for attachments in feedback UI +- [BUGFIX] `BITFeedbackManager`: Fixed statusbar issues and the image attachment picker with apps not showing a status bar +- [BUGFIX] Removed a header file from the crash only build that is not needed +- [BUGFIX] Fixed various typos in documentation, properties +- [BUGFIX] Fixed various compiler warnings +- [BUGFIX] Various additional fixes + +## Version 3.6.2 + +- [UPDATE] Store anonymous UUID asynchronously into the keychain to work around rare keychain blocking behavior +- [UPDATE] `BITCrashManager`: Improved detecting app specific binary images in crash report for improved crash grouping on the server +- [UPDATE] `BITUpdateManager`: Added new `updateManagerWillExitApp` delegate method +- [UPDATE] `BITUpdateManager`: Don't save any file when app was installed from App Store +- [BUGFIX] `BITCrashManager`: Fixed issues with sending crash reports for apps with xml tags in the app name +- [BUGFIX] `BITFeedbackManager`: Fixed screenshot trigger issue not always fetching the last taken image +- [BUGFIX] `BITFeedbackManager`: Fixed compose view issue with predefined text +- [BUGFIX] Fixed a warning when integrating the binary framework for only crash reporting +- [BUGFIX] Fixed compiler warnings +- [BUGFIX] Various additional fixes + +## Version 3.6.1 + +- [BUGFIX] Fixed feedback compose view to correctly show the text in landscape on iOS 8 + +## Version 3.6 + +- [NEW] `BITCrashManager`: Added support for iOS 8 Extensions +- [NEW] `BITCrashManager`: Option to add a custom UI flow before sending a crash report, e.g. to ask users for more details (see `setAlertViewHandler:`) +- [NEW] `BITCrashManager`: Provide details on a crash report (see `lastSessionCrashDetails` and `BITCrashDetails`) +- [NEW] `BITCrashManager`: Experimental support for detecting app kills triggered by iOS while the app is in foreground (see `enableAppNotTerminatingCleanlyDetection`) +- [NEW] `BITCrashManager`: Added `didReceiveMemoryWarningInLastSession` which indicates if the last app session did get a memory warning by iOS +- [NEW] `BITFeedbackManager`: Attach and annotate images and screenshots +- [NEW] `BITFeedbackManager`: Attach any binary data to compose message view (see `showFeedbackComposeViewWithPreparedItems:`) +- [NEW] `BITFeedbackManager`: Show a compose message with a screenshot image attached using predefined triggers (see `feedbackObservationMode`) or your own custom triggers (see `showFeedbackComposeViewWithGeneratedScreenshot`) +- [NEW] Minimum iOS Deployment version is now iOS 6.0 +- [NEW] Requires to link additional frameworks: `AssetLibrary`, `MobileCoreServices`, `QuickLook` +- [UPDATE] `BITCrashManager`: Updated `setCrashCallbacks` handling now using `BITCrashManagerCallbacks` instead of `PLCrashReporterCallbacks` (which is no longer public) +- [UPDATE] `BITCrashManager`: Crash reports are now sent individually if there are multiple pending +- [UPDATE] `BITUpdateManager`: Improved algorithm for fetching an optimal sized app icon for the Update View +- [UPDATE] `BITUpdateManager`: Properly consider paragraphs in release notes when presenting them in the Update view +- [UPDATE] Property `delegate` in all components is now private. Set the delegate on `BITHockeyManager` only! +- [UPDATE] Removed support for Atlassian JMC +- [BUGFIX] Various additional fixes +

+ +## Version 3.6.0 Beta 2 + +- [NEW] `BITFeedbackManager`: Screenshot feature is now part of the public API +- [UPDATE] `BITFeedbackManager`: Various improvements for the screenshot feature +- [UPDATE] `BITFeedbackManager`: Added `BITHockeyAttachment` for more customizable attachments to feedback (`content-type`, `filename`) +- [UPDATE] `BITUpdateManager`: Improved algorithm for fetching an optimal sized app icon for the Update View +- [UPDATE] `BITUpdateManager`: Properly consider paragraphs in releases notes when presenting them in the Update View +- [UPDATE] `BITCrashManager`: Updated PLCrashReporter to version 1.2 +- [UPDATE] `BITCrashManager`: Added `osVersion` and `osBuild` properties to `BITCrashDetails` +- [BUGFIX] `BITCrashManager`: Use correct filename for crash report attachments +- [UPDATE] Property `delegate` in all components is now private. Set the delegate on `BITHockeyManager` only! +- [BUGFIX] Various additional fixes +

+ +## Version 3.6.0 Beta 1 + +- [NEW] Minimum iOS Deployment version is now iOS 6.0 +- [NEW] Requires to link additional frameworks: `AssetLibrary`, `MobileCoreServices`, `QuickLook` +- [NEW] `BITFeedbackManager`: Attach and annotate images and screenshots +- [NEW] `BITFeedbackManager`: Attach any binary data to compose message view (see `showFeedbackComposeViewWithPreparedItems:`) +- [NEW] `BITFeedbackManager`: Show a compose message with a screenshot image attached using predefined triggers (see `feedbackObservationMode`) or your own custom triggers (see `showFeedbackComposeViewWithGeneratedScreenshot`) +- [NEW] `BITCrashManager`: Option to add a custom UI flow before sending a crash report, e.g. to ask users for more details (see `setAlertViewHandler:`) +- [NEW] `BITCrashManager`: Provide details on a crash report (see `lastSessionCrashDetails`) +- [NEW] `BITCrashManager`: Experimental support for detecting app kills triggered by iOS while the app is in foreground (see `enableAppNotTerminatingCleanlyDetection`) +- [NEW] `BITCrashManager`: Added `didReceiveMemoryWarningInLastSession` which indicates if the last app session did get a memory warning by iOS +- [UPDATE] `BITCrashManager`: Updated `setCrashCallbacks` handling now using `BITCrashManagerCallbacks` instead of `PLCrashReporterCallbacks` (which is no longer public) +- [UPDATE] `BITCrashManager`: Crash reports are now send individually if there are multiple pending +- [UPDATE] Removed support for Atlassian JMC +- [BUGFIX] Fixed an incorrect submission warning about referencing non-public selector `attachmentData` +

+ +## Version 3.5.7 + +- [UPDATE] Easy Swift integration for binary distribution (No Objective-C bridging header required) +- [UPDATE] `BITAuthenticator`: Improved keychain handling +- [UPDATE] `BITUpdateManager`: Improved iOS 8 In-App-Update process handling +- [BUGFIX] `BITUpdateManager`: Fixed layout issue for resizable iOS layout +- [BUGFIX] Fixed an iTunes Connect warning for `attachmentData` property +

+ +## Version 3.5.6 + +- [UPDATE] `BITCrashManager`: Updated PLCrashReporter to version 1.2 +- [UPDATE] `BITUpdateManager`: Improved algorithm to find the optimal app icon +- [BUGFIX] `BITAuthenticator`: Fixed problem with authorization and iOS 8 +- [BUGFIX] Fixed a problem with integration test and iOS 8 +

+ +## Version 3.5.5 + +- [NEW] `BITCrashManager`: Added support for adding a binary attachment to crash reports +- [NEW] `BITCrashManager`: Integrated PLCrashReporter 1.2 RC5 (with 2 more fixes) +- [BUGFIX] `BITUpdateManager`: Fixed problem with `checkForUpdate` when `updateSetting` is set to `BITUpdateCheckManually` +- [BUGFIX] `BITAuthenticator`: Fixed keychain warning alert showing app on launch if keychain is locked +- [BUGFIX] `BITAuthenticator`: Fixed a possible assertion problem with auto-authentication (when using custom SDK builds without assertions being disabled) +- [BUGFIX] `BITAuthenticator`: Added user email to crash report for beta builds if BITAuthenticator is set to BITAuthenticatorIdentificationTypeWebAuth +- [BUGFIX] Fixed more analyzer warnings +

+ +## Version 3.5.4 + +- [BUGFIX] Fix a possible crash before sending the crash report when the selector could not be found +- [BUGFIX] Fix a memory leak in keychain handling +

+ +## Version 3.5.3 + +- [NEW] Crash Reports now provide the selector name e.g. for crashes in `objc_MsgSend` +- [NEW] Add setter for global `userID`, `userName`, `userEmail`. Can be used instead of the delegates. +- [UPDATE] On device symbolication is now optional, disabled by default +- [BUGFIX] Fix for automatic authentication not always working correctly +- [BUGFIX] `BITFeedbackComposeViewControllerDelegate` now also works for compose view controller used by the feedback list view +- [BUGFIX] Fix typos in documentation +

+ +## Version 3.5.2 + +- [UPDATE] Make sure a log message appears in the console if the SDK is not setup on the main thread +- [BUGFIX] Fix usage time always being send as `0` instead of sending the actual usage time +- [BUGFIX] Fix "Install" button in the mandatory update alert not working and forcing users to use the "show" button and then install from the update view instead +- [BUGFIX] Fix possible unused function warnings +- [BUGFIX] Fix two warnings when `-Wshorten-64-to-32` is set. +- [BUGFIX] Fix typos in documentation +

+ +## Version 3.5.1 + +- General + + - [NEW] Add new initialize to make the configuration easier: `[BITHockeyManager configureWithIdentifier:]` + - [NEW] Add `[BITHockeyManager testIdentifier]` to check if the SDK reaches the server. The result is shown on the HockeyApp website on success. + - [UPDATE] `delegate` can now also be defined using the property directly (instead of using the configureWith methods) + - [UPDATE] Use system provided Base64 encoding implementation + - [UPDATE] Improved logic to choose the right `UIWindow` instance for dialogs + - [BUGFIX] Fix compile issues when excluding all modules but crash reporting + - [BUGFIX] Fix warning on implicit conversion from `CGImageAlphaInfo` to `CGBitmapInfo` + - [BUGFIX] Fix warnings for implicit conversions of `UITextAlignment` and `UILineBreakMode` + - [BUGFIX] Various additional smaller bug fixes +

+ +- Crash Reporting + + - [NEW] Integrated PLCrashReporter 1.2 RC 2 + - [NEW] Add `generateTestCrash` method to more quickly test the crash reporting (automatically disabled in App Store environment!) + - [NEW] Add PLCR header files to the public headers in the framework + - [NEW] Add the option to define callbacks that will be executed prior to program termination after a crash has occurred. Callback code has to be async-safe! + - [UPDATE] Change the default of `showAlwaysButton` property to `YES` + - [BUGFIX] Always format date and timestamps in crash report in `en_US_POSIX` locale. +

+ +- Feedback + + - [UPDATE] Use only one activity view controller per UIActivity + - [BUGFIX] Fix delete button appearance in feedback list view on iOS 7 when swiping a feedback message + - [BUGFIX] Comply to -[UIActivity activityDidFinish:] requirements + - [BUGFIX] Use non-deprecated delegate method for `BITFeedbackActivity` +

+ +- Ad-Hoc/Enterprise Authentication + + - [NEW] Automatic authorization when app was installed over the air. This still requires to call `[BITAuthenticator authenticateInstallation];` after calling `startManager`! + - [UPDATE] Set the tintColor in the auth view and modal views navigation controller on iOS 7 + - [UPDATE] Show an alert if the authentication token could not be stored into the keychain + - [UPDATE] Use UTF8 encoding for auth data + - [UPDATE] Replace email placeholder texts + - [BUGFIX] Make sure the authentication window is always correctly dismissed + - [BUGFIX] Fixed memory issues +

+ +- Ad-Hoc/Enterprise Updates + + - [NEW] Provide alert option to show mandatory update details + - [NEW] Add button to expired page (and alert) that lets the user check for a new version (can be disabled using `disableUpdateCheckOptionWhenExpired`) + - [UPDATE] Usage metrics are now stored in an independent file instead of using `NSUserDefaults` +

+ + +## Version 3.5.0 + +- General + + - [NEW] Added support for iOS 7 + - [NEW] Added support for arm64 architecture + - [NEW] Added `BITStoreUpdateManager` for alerting the user of available App Store updates (disabled by default) + - [NEW] Added `BITAuthenticator` class for authorizing installations (Ad-Hoc/Enterprise builds only!) + - [NEW] Added support for apps starting in the background + - [NEW] Added possibility to build custom frameworks including/excluding specific modules from the static library (see `HockeySDKFeatureConfig.h`) + - [NEW] Added public access to the anonymous UUID that the SDK generates per app installation + - [NEW] Added possibility to overwrite SDK specific localization strings in the apps localization files + - [UPDATE] Updated localizations provided by [Wordcrafts.de](http://wordcrafts.de): + Chinese, Dutch, English, French, German, Hungarian, Italian, Japanese, Portuguese, Brazilian-Portuguese, Romanian, Russian, Spanish + - [UPDATE] User related data is now stored in the keychain instead of property files + - [UPDATE] SDK documentation improvements + - [BUGFIX] Fixed multiple compiler warnings + - [BUGFIX] Various UI updates and fixes +

+ +- Crash Reporting + + - [NEW] Integrated PLCrashReporter 1.2 beta 3 + - [NEW] Added optional support for Mach exceptions + - [NEW] Added support for arm64 + - [UPDATE] PLCrashReporter build with `BIT` namespace to avoid collisions + - [UPDATE] Crash reporting is automatically disabled when the app is invoked with the debugger! + - [UPDATE] Automatically add the users UDID or email to crash reports in Ad-Hoc/Enterprise builds if they are provided by BITAuthenticator +

+ +- Feedback + + - [NEW] New protocol to inform about incoming feedback messages, see `BITFeedbackManagerDelegate` + - [UPDATE] Added method in `BITFeedbackComposeViewControllerDelegate` to let the app know if the user submitted a new message or cancelled it +

+ +- App Store Updates + + - [NEW] Inform user when a new version is available in the App Store (optional, disabled by default) +

+ + +- Ad-Hoc/Enterprise Authentication + + - [NEW] `BITAuthenticator` identifies app installations, automatically disabled in App Store environments + - [NEW] `BITAuthenticator` can identify the user through: + - The email address of their HockeyApp account + - Login with their HockeyApp account (does not work with Facebook accounts!) + - Installation of the HockeyApp web-clip to provide the UDID (requires the app to handle URL callbacks) + - Web based login with their HockeyApp account + - [NEW] `BITAuthenticator` can require the authorization: + - Never + - On first app version launch + - Whenever the app comes into foreground (requires the device to have a working internet connection) + - [NEW] Option to customize the authentication flow + - [NEW] Possibility to use an existing URL scheme +

+ +- Ad-Hoc/Enterprise Updates + + - [UPDATE] Removed delegate for getting the UDID, please migrate to the new `BITAuthenticator` + - [NEW] In-app updates are now only offered if the device matches the minimum OS version requirement +

+ +--- + +## Version 3.5.0 RC 3 + +- General + + - [NEW] Added public access to the anonymous UUID that the SDK generates per app installation + - [NEW] Added possibility to overwrite SDK specific localization strings in the apps localization files + - [UPDATE] Podspec updates + - [BUGFIX] Fixed memory leaks + - [BUGFIX] Various minor bugfixes +

+ +- Crash Reporting + + - [UPDATE] Integrated PLCrashReporter 1.2 beta 3 + - [BUGFIX] Fixed crash if minimum OS version isn't provided + - [BUGFIX] Update private C function to use BIT namespace +

+ +- Feedback + + - [BUGFIX] Fixed some layout issues in the user info screen +

+ +- Ad-Hoc/Enterprise Updates + + - [BUGFIX] Fixed update view controller not showing updated content after using the check button + - [BUGFIX] Fixed usage value being reset on every app cold start +

+ +- Ad-Hoc/Enterprise Authentication + + - [NEW] Added web based user authentication + - [UPDATE] IMPORTANT: You need to call `[[BITHockeyManager sharedHockeyManager].authenticator authenticateInstallation];` yourself after startup when the authentication and/or verification should be performed and when it is safe to present a modal view controller! + - [UPDATE] Removed `automaticMode`. You now need to call `authenticateInstallation` when it is safe to do so or handle the complete process yourself. +

+ +## Version 3.5.0 RC 2 + +- General + + - [BUGFIX] Remove assertions from release build +

+ +- Ad-Hoc/Enterprise Updates + + - [BUGFIX] Add new iOS 7 icon sizes detection and adjust corner radius +

+ +## Version 3.5.0 RC 1 + +- General + + - [UPDATE] Documentation improvements nearly everywhere +

+ +- Crash Reporting + + - [UPDATE] Integrated PLCrashReporter 1.2 beta 2 + - [UPDATE] 64 bit crash reports now contain the correct architecture string + - [UPDATE] Automatically add the users UDID or email to crash reports in Ad-Hoc/Enterprise builds if they are provided by BITAuthenticator + - [BUGFIX] Fixed userName, userEmail and userID not being added to crash reports +

+ +- App Store Updates + + - [UPDATE] Changed default update check interval to weekly +

+ +- Ad-Hoc/Enterprise Authentication + + - [NEW] Redesigned API for easier usage and more flexibility (please check the documentation!) + - [NEW] Added option to customize the authentication flow + - [NEW] Added option to provide a custom parentViewController for presenting the UI + - [NEW] Added possibility to use an existing URL scheme + - [BUGFIX] Fixed authentication UI appearing after updating apps without changing the authentication settings +

+ +- Ad-Hoc/Enterprise Updates + + - [UPDATE] Don't add icon gloss to icons when running on iOS 7 + - [BUGFIX] Fixed a few iOS 7 related UI problems in the update view +

+ + +## Version 3.5.0 Beta 3 + +- Feedback + + - [BUGFIX] Fix a layout issue with the compose feedback UI on the iPad with iOS 7 in landscape orientation +

+ +- Ad-Hoc/Enterprise Authentication + + - [BUGFIX] Fix a possible crash in iOS 5 +

+ + +## Version 3.5.0 Beta 2 + +- General + + - [NEW] Added support for apps starting in the background + - [UPDATE] Added updated CocoaSpec + - [BUGFIX] Various documentation improvements +

+ +- Ad-Hoc/Enterprise Authentication + + - [BUGFIX] Fix duplicate count of installations +

+ +- Ad-Hoc/Enterprise Updates + + - [BUGFIX] Update view not showing any versions + - [BUGFIX] Fix a crash presenting the update view on iOS 5 and iOS 6 +

+ + +## Version 3.5.0 Beta 1 + +- General + + - [NEW] Added support for iOS 7 + - [NEW] Added experimental support for arm64 architecture + - [NEW] Added `BITStoreUpdateManager` for alerting the user of available App Store updates (disabled by default) + - [NEW] Added `BITAuthenticator` class for authorizing installations (Ad-Hoc/Enterprise builds only!) + - [NEW] Added possibility to build custom frameworks including/excluding specific modules from the static library (see `HockeySDKFeatureConfig.h`) + - [UPDATE] User related data is now stored in the keychain instead of property files + - [UPDATE] SDK documentation improvements + - [BUGFIX] Fixed multiple compiler warnings + - [BUGFIX] Fixed a few UI glitches, e.g. adjusting status bar style +

+ +- Crash Reporting + + - [NEW] Integrated PLCrashReporter 1.2 beta 1 + - [NEW] Added optional support for Mach exceptions + - [NEW] Experimental support for arm64 (will be tested and improved once devices are available) + - [UPDATE] PLCrashReporter build with `BIT` namespace to avoid collisions + - [UPDATE] Crash reporting is automatically disabled when the app is invoked with the debugger! +

+ +- Feedback + + - [NEW] New protocol to inform about incoming feedback messages, see `BITFeedbackManagerDelegate` + - [UPDATE] Added method in `BITFeedbackComposeViewControllerDelegate` to let the app know if the user submitted a new message or cancelled it +

+ +- App Store Updates + + - [NEW] Inform user when a new version is available in the App Store (optional, disabled by default) +

+ +- Ad-Hoc/Enterprise Updates and Authentication + + - [UPDATE] Removed delegate for getting the UDID, please migrate to the new `BITAuthenticator` + - [NEW] In-app updates are now only offered if the device matches the minimum OS version requirement + - [NEW] `BITAuthenticator` identifies app installations, automatically disabled in App Store environments + - [NEW] `BITAuthenticator` can identify the user through: + - The email address of his/her HockeyApp account + - Login with his/her HockeyApp account (does not work with Facebook accounts!) + - Installation of the HockeyApp web-clip to provide the UDID (requires the app to handle URL callbacks) + - [NEW] `BITAuthenticator` can require the authorization: + - Never + - Optionally, i.e. the user can skip the dialog + - On first app version launch + - Whenever the app comes into foreground (requires the device to have a working internet connection) +

+ + +## Version 3.0.0 + +- General + + - [NEW] Added new Feedback module + - [NEW] Minimum iOS Deployment version is now iOS 5.0 + - [NEW] Migrated to use ARC + - [NEW] Added localizations provided by [Wordcrafts.de](http://wordcrafts.de): + Chinese, English, French, German, Italian, Japanese, Portuguese, Brazilian-Portuguese, Russian, Spanish + - [NEW] Added Romanian, Hungarian localization + - [UPDATE] Updated integration and migration documentation + - [Installation & Setup](http://www.hockeyapp.net/help/sdk/ios/3.0.0/docs/docs/Guide-Installation-Setup.html) (Recommended) + - [Installation & Setup Advanced](http://www.hockeyapp.net/help/sdk/ios/3.0.0/docs/docs/Guide-Installation-Setup-Advanced.html) (Using Git submodule and Xcode sub-project) + - [Migration from previous SDK Versions](http://www.hockeyapp.net/help/sdk/ios/3.0.0/docs/docs/Guide-Migration-Kits.html) + - [UPDATE] Using embedded.framework for binary distribution containing everything needed in one package + - [UPDATE] Improved Xcode project setup to only use one static library + - [UPDATE] Providing build settings as `HockeySDK.xcconfig` file for easier setup + - [UPDATE] Remove `-ObjC` from `Other Linker Flags`, since the SDK doesn't need it anymore + - [UPDATE] Improved documentation + - [UPDATE] Excluded binary UUID check from simulator builds, so unit test targets will work. But functionality based on binary UUID cannot be tested in the simulator, e.g. update without changing build version. + - [BUGFIX] Fixed some new compiler warnings + - [BUGFIX] Fixed some missing new lines at EOF + - [BUGFIX] Make sure sure JSON serialization doesn't crash if the string is nil + - [BUGFIX] Various additional minor fixes +

+ +- Crash Reporting + + - [NEW] Added anonymous device ID to crash reports + - [UPDATE] The following delegates in `BITCrashManagerDelegate` moved to `BITHockeyManagerDelegate`: + - `- (NSString *)userNameForCrashManager:(BITCrashManager *)crashManager;` is now `- (NSString *)userNameForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager;` + - `- (NSString *)userEmailForCrashManager:(BITCrashManager *)crashManager;` is now `- (NSString *)userEmailForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager;` + - [BUGFIX] Moved calculation of time interval between startup and crash further up in the code, so delegates can use this information e.g. to add it into a log file + - [BUGFIX] If a crash was detected but could not be read (if handling crashes on startup is implemented), the delegate is still called + - [BUGFIX] Timestamp in crash report is now always UTC in en_US locale + - [BUGFIX] Make sure crash reports incident identifier and key don't have special [] chars and some value +

+ +- Feedback + + - [NEW] User feedback interface for direct communication with your users + - [NEW] iOS 6 UIActivity component for integrating feedback + - [NEW] When first opening the feedback list view, user details and show compose screen are automatically shown +

+ +- Updating + + - [NEW] Support for In-App updates without changing `CFBundleVersion` + - [UPDATE] Update UI modified to be more iOS 6 alike + - [UPDATE] Update UI shows the company name next to the app name if defined in the backend + - [UPDATE] Updated integration and migration documentation: [Installation & Setup](http://www.hockeyapp.net/help/sdk/ios/3.0.0/docs/docs/Guide-Installation-Setup.html) (Recommended), [Installation & Setup Advanced](http://www.hockeyapp.net/help/sdk/ios/3.0.0/docs/docs/Guide-Installation-Setup-Advanced.html) (Using Git submodule and Xcode sub-project), [Migration from previous SDK Versions](http://www.hockeyapp.net/help/sdk/ios/3.0.0/docs/docs/Guide-Migration-Kits.html) + + - [BUGFIX] Fixed a problem showing the update UI animated if there TTNavigator class is present even though not being used + +--- + +### Version 3.0.0 RC 1 + +- General: + + - [NEW] Added localizations provided by [Wordcrafts.de](http://wordcrafts.de): + Chinese, English, French, German, Italian, Japanese, Portuguese, Brazilian-Portuguese, Russian, Spanish + - [NEW] Added Romanian localization + - [UPDATE] Documentation improvements + - [UPDATE] Exclude binary UUID check from simulator builds, so unit test targets will work. But functionality based on binary UUID cannot be tested in the simulator, e.g. update without changing build version. + - [BUGFIX] Cocoapods bugfix for preprocessor definitions + - [BUGFIX] Various additional minor fixes + +- Feedback: + + - [UPDATE] Only push user details screen automatically onto the list view once + - [BUGFIX] Show proper missing user name or email instead of showing `(null)` in a button + - [BUGFIX] Various fixes to changing the `requireUserEmail` and `requireUserName` values + + +### Version 3.0.0b5 + +- General: + + - [NEW] Remove `-ObjC` from `Other Linker Flags`, since the SDK doesn't need it + - [NEW] Update localizations (german, croatian) + - [BUGFIX] Fix some new compiler warnings + - [BUGFIX] Fix some missing new lines at EOF + - [BUGFIX] Make sure sure JSON serialization doesn't crash if the string is nil + +- Crash Reporting: + + - [NEW] Add anonymous device ID to crash reports + - [BUGFIX] Move calculation of time interval between startup and crash further up in the code, so delegates can use this information e.g. to add it into a log file + - [BUGFIX] Call delegate also if a crash was detected but could not be read (if handling crashes on startup is implemented) + - [BUGFIX] Format timestamp in crash report to be always UTC in en_US locale + - [BUGFIX] Make sure crash reports incident identifier and key don't have special [] chars and some value + +- Feedback: + + - [NEW] Ask user details and show compose screen automatically on first opening feedback list view + - [BUGFIX] Fix some users own messages re-appearing after deleting them + - [BUGFIX] Problems displaying feedback list view in a navigation hierarchy + +- Updating: + + - [BUGFIX] Fix a problem showing the update UI animated if there TTNavigator class is present even though not being used + +### Version 3.0.0b4 + +- Crash Reporting: + + - [BUGFIX] Fix a crash if `username`, `useremail` or `userid` delegate method returns `nil` and trying to send a crash report + +- Feedback: + + - [BUGFIX] Fix user data UI not always being presented as a form sheet on the iPad + +- Updating: + + - [BUGFIX] Fix a problem showing the update UI animated if there TTNavigator class is present even though not being used + +### Version 3.0.0b3 + +- General: + + - [BUGFIX] Exchange some more prefixes of TTTAttributedLabel class that have been missed out + - [BUGFIX] Fix some new compiler warnings + +- Crash Reporting: + + - [BUGFIX] Format timestamp in crash report to be always UTC in en_US locale + +### Version 3.0.0b2 + +- General: + + - [BUGFIX] Add missing header files to the binary distribution + - [BUGFIX] Add missing new lines of two header files + +### Version 3.0.0b1 + +- General: + + - [NEW] Feedback component + - [NEW] Minimum iOS Deployment version is now iOS 5.0 + - [NEW] Migrated to use ARC + - [UPDATE] Improved Xcode project setup to only use one static library + - [UPDATE] Providing build settings as `HockeySDK.xcconfig` file for easier setup + - [UPDATE] Using embedded.framework for binary distribution containing everything needed in one package + +- Feedback: + + - [NEW] User feedback interface for direct communication with your users + - [NEW] iOS 6 UIActivity component for integrating feedback + +- Updating: + + - [NEW] Support for In-App updates without changing `CFBundleVersion` + - [UPDATE] Update UI modified to be more iOS 6 alike + - [UPDATE] Update UI shows the company name next to the app name if defined in the backend + + +## Version 2.5.5 + +- General: + + - [BUGFIX] Fix some new compiler warnings + +- Crash Reporting: + + - [NEW] Add anonymous device ID to crash reports + - [BUGFIX] Move calculation of time interval between startup and crash further up in the code, so delegates can use this information e.g. to add it into a log file + - [BUGFIX] Call delegate also if a crash was detected but could not be read (if handling crashes on startup is implemented) + - [BUGFIX] Format timestamp in crash report to be always UTC in en_US locale + - [BUGFIX] Make sure crash reports incident identifier and key don't have special [] chars and some value + +- Updating: + + - [BUGFIX] Fix a problem showing the update UI animated if there TTNavigator class is present even though not being used + +## Version 2.5.4 + +- General: + + - Declared as final release, since everything in 2.5.4b3 is working as expected + +### Version 2.5.4b3 + +- General: + + - [NEW] Atlassian JMC support disabled (Use subproject integration if you want it) + +### Version 2.5.4b2 + +- Crash Reporting: + + - [UPDATE] Migrate pre v2.5 auto send user setting + - [BUGFIX] The alert option 'Auto Send' did not persist correctly + +- Updating: + + - [BUGFIX] Authorization option did not persist correctly and caused authorization to re-appear on every cold app start + +### Version 2.5.4b1 + +- General: + + - [NEW] JMC support is removed from binary distribution, requires the compiler preprocessor definition `JIRA_MOBILE_CONNECT_SUPPORT_ENABLED=1` to be linked. Enabled when using the subproject + - [BUGFIX] Fix compiler warnings when using Cocoapods + +- Updating: + + - [BUGFIX] `expiryDate` property not working correctly + +## Version 2.5.3 + +- General: + + - [BUGFIX] Fix checking validity of live identifier not working correctly + +## Version 2.5.2 + +- General: + + - Declared as final release, since everything in 2.5.2b2 is working as expected + +### Version 2.5.2b2 + +- General: + + - [NEW] Added support for armv7s architecture + +- Updating: + + - [BUGFIX] Fix update checks not done when the app becomes active again + + +### Version 2.5.2b1 + +- General: + + - [NEW] Replace categories with C functions, so the `Other Linker Flag` `-ObjC` and `-all_load` won't not be needed for integration + - [BUGFIX] Some code style fixes and missing new lines in headers at EOF + +- Crash Reporting: + + - [NEW] PLCrashReporter framework now linked into the HockeySDK framework, so that won't be needed to be added separately any more + - [NEW] Add some error handler detection to optionally notify the developer of multiple handlers that could cause crashes not to be reported to HockeyApp + - [NEW] Show an error in the console if an older version of PLCrashReporter is linked + - [NEW] Make sure the app doesn't crash if the developer forgot to delete the old PLCrashReporter version and the framework search path is still pointing to it + +- Updating: + + - [BUGFIX] Fix disabling usage tracking and expiry check not working if `checkForUpdateOnLaunch` is set to NO + - [BUGFIX] `disableUpdateManager` wasn't working correctly + - [BUGFIX] If the server doesn't return any app versions, don't handle this as an error, but show a warning in the console when `debugLogging` is enabled + +## Version 2.5.1 + +- General: + + - [BUGFIX] Typo in delegate `shouldUseLiveIdentifier` of `BITHockeyManagerDelegate` + - [BUGFIX] Default updateManager delegate wasn't set + +- Crash Reporting: + + - [BUGFIX] Crash when developer sends the notification `BITHockeyNetworkDidBecomeReachableNotification` + + +## Version 2.5 + +- General: + + - [NEW] Unified SDK for accessing HockeyApp on iOS + + - Requires iOS 4.0 or newer + + - Replaces the previous separate SDKs for iOS: HockeyKit and QuincyKit. + + The previous SDKs are still available and are still working. But future + HockeyApp features will only be integrated in this new unified SDK. + + - Integration either as framework or Xcode subproject using the sourcecode + + Check out [Installation & Setup](Guide-Installation-Setup) + + - [NEW] Cleaned up public interfaces and internal processing all across the SDK + + - [NEW] [AppleDoc](http://gentlebytes.com/appledoc/) based documentation and HowTos + + This allows the documentation to be generated into HTML or DocSet. + +- Crash Reporting: + + - [NEW] Workflow to handle crashes that happen on startup. + + Check out [How to handle crashes on startup](HowTo-Handle-Crashes-On-Startup) for more details. + + - [NEW] Symbolicate iOS calls async-safe on the device + + - [NEW] Single property/option to deactivate, require user to agree submitting and autosubmit + + E.g. implement a settings screen with the three options and set + `[BITCrashManager crashManagerStatus]` to the desired user value. + + - [UPDATED] Updated [PLCrashReporter](https://code.google.com/p/plcrashreporter/) with updates and bugfixes (source available on [GitHub](https://github.com/bitstadium/PLCrashReporter)) + + - [REMOVED] Feedback for Crash Groups Status + + Please keep using QuincyKit for now if you want this feature. This feature needs to be + redesigned on SDK and server side to be more efficient and easier to use. + +- Updating: + + - [NEW] Expire beta versions with a given date + + - [REMOVED] Settings screen + + If you want users to be able not to send analytics data, implement the + `[BITUpdateManagerDelegate updateManagerShouldSendUsageData:]` delegate and return + the value depending on what the user defines in your settings UI. diff --git a/submodules/HockeySDK-iOS/Documentation/Guides/Crash Reporting Not Working.md b/submodules/HockeySDK-iOS/Documentation/Guides/Crash Reporting Not Working.md new file mode 100644 index 0000000000..5ed8718983 --- /dev/null +++ b/submodules/HockeySDK-iOS/Documentation/Guides/Crash Reporting Not Working.md @@ -0,0 +1,26 @@ +## Crash Reporting is not working + +This is a checklist to help find the issue if crashes do not appear in HockeyApp or the dialog asking if crashes should be send doesn't appear: + + +1. Check if the `BETA_IDENTIFIER` or `LIVE_IDENTIFIER` matches the App ID in HockeyApp. + +2. Check if CFBundleIdentifier in your Info.plist matches the Bundle Identifier of the app in HockeyApp. HockeyApp accepts crashes only if both the App ID and the Bundle Identifier equal their corresponding values in your plist and source code. + +3. Unless you have set `[BITCrashManager setCrashManagerStatus:]` to `BITCrashManagerStatusAutoSend`: If your app crashes and you start it again, is the alert shown which asks the user to send the crash report? If not, please crash your app again, then connect the debugger and set a break point in `BITCrashManager.m`, method `startManager` to see why the alert is not shown. + +4. Enable the debug logging option and check the output if the Crash Manager gets `Setup`, `Started`, returns no error message and sending the crash report to the server results in no error: + + [[BITHockeyManager shareHockeyManager] setDebugLogEnabled: YES]; + + +5. Make sure Xcode debugger is not attached while causing the app to crash + +6. Are you trying to catch "out of memory crashes"? This is _NOT_ possible! Out of memory crashes are actually kills by the watchdog process. Whenever you kill a process, there is no crash happening. The crash reports for those that you see on iTunes Connect, are arbitrary reports written by the watchdog process that did the kill. So they only system that can provide information about these, is iOS itself. + +7. If you are using `#ifdef (CONFIGURATION_something)`, make sure that the `something` string matches the exact name of your Xcode build configuration. Spaces are not allowed! + +8. Remove or at least disable any other exception handler or crash reporting framework. + +9. If it still does not work, please [contact us](http://support.hockeyapp.net/discussion/new). + diff --git a/submodules/HockeySDK-iOS/Documentation/Guides/Installation & Setup.md b/submodules/HockeySDK-iOS/Documentation/Guides/Installation & Setup.md new file mode 100644 index 0000000000..2ca0440c4c --- /dev/null +++ b/submodules/HockeySDK-iOS/Documentation/Guides/Installation & Setup.md @@ -0,0 +1,883 @@ + [![Build Status](https://www.bitrise.io/app/30bf519f6bd0a5e2/status.svg?token=RKqHc7-ojjLiEFds53d-ZA&branch=master)](https://www.bitrise.io/app/30bf519f6bd0a5e2) +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) +[![Version](http://cocoapod-badges.herokuapp.com/v/HockeySDK/badge.png)](http://cocoadocs.org/docsets/HockeySDK) + [![Slack Status](https://slack.hockeyapp.net/badge.svg)](https://slack.hockeyapp.net) + +## Version 5.1.2 + +- [Changelog](http://www.hockeyapp.net/help/sdk/ios/5.1.2/docs/docs/Changelog.html) + +**NOTE** If your are using the binary integration of our SDK, make sure that the `HockeySDKResources.bundle` inside the `HockeySDK.embeddedframework`-folder has been added to your application. + +### Feedback and iOS 10 +**4.1.1 and later of the HockeySDK remove the Feedback feature from the default version of the SDK.** +The reason for this is that iOS 10 requires developers to add a usage string to their Info.plist in case they include the photos framework in their app. If this string is missing, the app will be rejected when submitting the app to the app store. As HockeyApp's Feedback feature includes a dependency to the photos framework. This means that if you include HockeyApp into your app, adding the usage string would be a requirement even for developers who don't use the Feedback feature. If you don't use Feedback in your app, simply upgrade HockeySDK to version 4.1.1 or newer. If you are using Feedback, please have a look at the [Feedback section](#feedback). + + +We **strongly** suggest upgrading to version 4.1.1 or a later version of the SDK. Not specifying the usage description string and using previous versions of the HockeySDK-iOS will cause the app to crash at runtime as soon as the user taps the "attach image"-button or in case you have enabled `BITFeedbackObservationModeOnScreenshot`. + +If you are using an older version of the SDK, you must add a `NSPhotoLibraryUsageDescription` to your `Info.plist` to avoid a AppStore rejection during upload of your app (please have a look at the [Feedback section](#feedback)). + +## Introduction + +HockeySDK-iOS implements support for using HockeyApp in your iOS applications. + +The following features are currently supported: + +1. **Collect crash reports:** If your app crashes, a crash log with the same format as from the Apple Crash Reporter is written to the device's storage. If the user starts the app again, they are asked to submit the crash report to HockeyApp. This works for both beta and live apps, i.e. those submitted to the App Store. + +2. **User Metrics:** Understand user behavior to improve your app. Track usage through daily and monthly active users, monitor crash impacted users, as well as customer engagement through session count.You can now track **Custom Events** in your app, understand user actions and see the aggregates on the HockeyApp portal. + +3. **Update Ad-Hoc / Enterprise apps:** The app will check with HockeyApp if a new version for your Ad-Hoc or Enterprise build is available. If yes, it will show an alert view to the user and let them see the release notes, the version history and start the installation process right away. + +4. **Update notification for app store:** The app will check if a new version for your app store release is available. If yes, it will show an alert view to the user and let them open your app in the App Store app. (Disabled by default!) + +5. **Feedback:** Collect feedback from your users from within your app and communicate directly with them using the HockeyApp backend. + +6. **Authenticate:** Identify and authenticate users of Ad-Hoc or Enterprise builds + +This document contains the following sections: + +1. [Requirements](#requirements) +2. [Setup](#setup) +3. [Advanced Setup](#advancedsetup) + 1. [Linking System Frameworks manually](#linkmanually) + 2. [CocoaPods](#cocoapods) + 3. [Carthage](#carthage) + 4. [iOS Extensions](#extensions) + 5. [WatchKit 1 Extensions](#watchkit) + 6. [Crash Reporting](#crashreporting) + 7. [User Metrics](#user-metrics) + 8. [Feedback](#feedback) + 9. [Store Updates](#storeupdates) + 10. [In-App-Updates (Beta & Enterprise only)](#betaupdates) + 11. [Debug information](#debug) +4. [Documentation](#documentation) +5. [Troubleshooting](#troubleshooting) +6. [Contributing](#contributing) + 1. [Development Environment](#developmentenvironment) + 2. [Code of Conduct](#codeofconduct) + 3. [Contributor License](#contributorlicense) +7. [Contact](#contact) + + +## 1. Requirements + +1. We assume that you already have a project in Xcode and that this project is opened in Xcode 8 or later. +2. The SDK supports iOS 8.0 and later. + + +## 2. Setup + +We recommend integration of our binary into your Xcode project to setup HockeySDK for your iOS app. You can also use our interactive SDK integration wizard in HockeyApp for Mac which covers all the steps from below. For other ways to setup the SDK, see [Advanced Setup](#advancedsetup). + +### 2.1 Obtain an App Identifier + +Please see the "[How to create a new app](http://support.hockeyapp.net/kb/about-general-faq/how-to-create-a-new-app)" tutorial. This will provide you with an HockeyApp specific App Identifier to be used to initialize the SDK. + +### 2.2 Download the SDK + +1. Download the latest [HockeySDK-iOS](http://www.hockeyapp.net/releases/) framework which is provided as a zip-File. +2. Unzip the file and you will see a folder called `HockeySDK-iOS`. (Make sure not to use 3rd party unzip tools!) + +### 2.3 Copy the SDK into your projects directory in Finder + +From our experience, 3rd-party libraries usually reside inside a subdirectory (let's call our subdirectory `Vendor`), so if you don't have your project organized with a subdirectory for libraries, now would be a great start for it. To continue our example, create a folder called `Vendor` inside your project directory and move the unzipped `HockeySDK-iOS`-folder into it. + +The SDK comes in four flavors: + + * Default SDK without Feedback: `HockeySDK.embeddedframework` + * Full featured SDK with Feedback: `HockeySDK.embeddedframework` in the subfolder `HockeySDKAllFeatures`. + * Crash reporting only: `HockeySDK.framework` in the subfolder `HockeySDKCrashOnly`. + * Crash reporting only for extensions: `HockeySDK.framework` in the subfolder `HockeySDKCrashOnlyExtension` (which is required to be used for extensions). + +Our examples will use the **default** SDK (`HockeySDK.embeddedframework`). + + + +### 2.4 Add the SDK to the project in Xcode + +> We recommend using Xcode's group-feature to create a group for 3rd-party-libraries similar to the structure of our files on disk. For example, similar to the file structure in 2.3 above, our projects have a group called `Vendor`. + +1. Make sure the `Project Navigator` is visible (⌘+1). +2. Drag & drop `HockeySDK.embeddedframework` from your `Finder` to the `Vendor` group in `Xcode` using the `Project Navigator` on the left side. +3. An overlay will appear. Select `Create groups` and set the checkmark for your target. Then click `Finish`. + + +### 2.5 Modify Code + +**Objective-C** + +1. Open your `AppDelegate.m` file. +2. Add the following line at the top of the file below your own `import` statements: + + ```objc + @import HockeySDK; + ``` + +3. Search for the method `application:didFinishLaunchingWithOptions:` +4. Add the following lines to setup and start the HockeyApp SDK: + + ```objc + [[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"APP_IDENTIFIER"]; + // Do some additional configuration if needed here + [[BITHockeyManager sharedHockeyManager] startManager]; + [[BITHockeyManager sharedHockeyManager].authenticator authenticateInstallation]; // This line is obsolete in the crash only builds + ``` + +**Swift** + +1. Open your `AppDelegate.swift` file. +2. Add the following line at the top of the file below your own import statements: + + ```swift + import HockeySDK + ``` + +3. Search for the method + + ```swift + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool + ``` + +4. Add the following lines to setup and start the HockeyApp SDK: + + ```swift + BITHockeyManager.shared().configure(withIdentifier: "APP_IDENTIFIER") + BITHockeyManager.shared().start() + BITHockeyManager.shared().authenticator.authenticateInstallation() // This line is obsolete in the crash only builds + + ``` + + +*Note:* The SDK is optimized to defer everything possible to a later time while making sure e.g. crashes on start-up can also be caught and each module executes other code with a delay some seconds. This ensures that `applicationDidFinishLaunching` will process as fast as possible and the SDK will not block the start-up sequence resulting in a possible kill by the watchdog process. + + +**Congratulation, now you're all set to use HockeySDK!** + + +## 3. Advanced Setup + + +### 3.1 Linking System Frameworks manually + +If you are working with an older project which doesn't support clang modules yet or you for some reason turned off the `Enable Modules (C and Objective-C` and `Link Frameworks Automatically` options in Xcode, you have to manually link some system frameworks: + +1. Select your project in the `Project Navigator` (⌘+1). +2. Select your app target. +3. Select the tab `Build Phases`. +4. Expand `Link Binary With Libraries`. +5. Add the following system frameworks, if they are missing: + 1. Default SDK: + + `CoreText` + + `CoreGraphics` + + `Foundation` + + `MobileCoreServices` + + `QuartzCore` + + `QuickLook` + + `Security` + + `SystemConfiguration` + + `UIKit` + + `libc++` + + `libz` + 2. SDK with all features: + + `CoreText` + + `CoreGraphics` + + `Foundation` + + `MobileCoreServices` + + `QuartzCore` + + `QuickLook` + + `Photos` + + `Security` + + `SystemConfiguration` + + `UIKit` + + `libc++` + + `libz` + 3. Crash reporting only: + + `Foundation` + + `Security` + + `SystemConfiguration` + + `UIKit` + + `libc++` + 4. Crash reporting only for extensions: + + `Foundation` + + `Security` + + `SystemConfiguration` + + `libc++` + +Note that not using clang modules also means that you can't use the `@import` syntax mentioned in the [Modify Code](#modify) section but have to stick to the old `#import ` imports. + + +### 3.2 CocoaPods + +[CocoaPods](http://cocoapods.org) is a dependency manager for Objective-C, which automates and simplifies the process of using 3rd-party libraries like HockeySDK in your projects. To learn how to setup CocoaPods for your project, visit the [official CocoaPods website](http://cocoapods.org/). + +**Podfile** + +```ruby +platform :ios, '8.0' +pod "HockeySDK" +``` + +#### 3.2.1 Binary Distribution Options + +The default and recommended distribution is a binary (static library) and a resource bundle with translations and images for all SDK features. + +```ruby +platform :ios, '8.0' +pod "HockeySDK" +``` + +Will integrate the *default* configuration of the SDK, with all features except the Feedback feature. + +For the SDK with all features, including Feedback, add + +```ruby +pod "HockeySDK", :subspecs => ['AllFeaturesLib'] +``` +to your podfile. + +To add the variant that only includes crash reporting, use + +```ruby +pod "HockeySDK", :subspecs => ['CrashOnlyLib'] +``` + +Or you can use the Crash Reporting build only for extensions by using the following line in your `Podfile`: + +```ruby +pod "HockeySDK", :subspecs => ['CrashOnlyExtensionsLib'] +``` + +#### 3.2.2 Source Integration Options + +Alternatively, you can integrate the SDK by source if you want to do modifications or want a different feature set. The following entry will integrate the SDK: + +```ruby +pod "HockeySDK-Source" +``` + + +### 3.3 Carthage + +[Carthage](https://github.com/Carthage/Carthage) is an alternative way to add frameworks to your app. For general information about how to use Carthage, please follow their [documentation](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application). + +To add HockeySDK to your project, simply put this line into your `Cartfile`: + +`github "bitstadium/HockeySDK-iOS"` + +and then follow the steps described in the [Carthage documentation](https://github.com/Carthage/Carthage#if-youre-building-for-ios-tvos-or-watchos). + +This will integrate the **full-featured SDK** so you must include the `NSPhotoLibraryUsageDescription` and read the [feedback section](#feedback). If you want to include any other version of the SDK, version 4.1.4 added the ability to do that. You need to specify the configuration that you want to use + +#### Version without Feedback + +`carthage build --platform iOS --configuration ReleaseDefault HockeySDK-iOS` + +#### Crash-only version + +`carthage build --platform iOS --configuration ReleaseCrashOnly HockeySDK-iOS` + +### Crash-only extension + +`carthage build --platform iOS --configuration ReleaseCrashOnlyExtension HockeySDK-iOS` + + +### 3.4 iOS Extensions + +The following points need to be considered to use the HockeySDK SDK with iOS Extensions: + +1. Each extension is required to use the same values for version (`CFBundleShortVersionString`) and build number (`CFBundleVersion`) as the main app uses. (This is required only if you are using the same `APP_IDENTIFIER` for your app and extensions). +2. You need to make sure the SDK setup code is only invoked **once**. Since there is no `applicationDidFinishLaunching:` equivalent and `viewDidLoad` can run multiple times, you need to use a setup like the following example: + +**Objective-C** + + ```objc + static BOOL didSetupHockeySDK = NO; + + @interface TodayViewController () + + @end + + @implementation TodayViewController + + + (void)viewDidLoad { + [super viewDidLoad]; + if (!didSetupHockeySDK) { + [[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"APP_IDENTIFIER"]; + [[BITHockeyManager sharedHockeyManager] startManager]; + didSetupHockeySDK = YES; + } + } + ``` + + **Swift** + + ```swift + class TodayViewController: UIViewController, NCWidgetProviding { + + static var didSetupHockeySDK = false; + + override func viewDidLoad() { + super.viewDidLoad() + if !TodayViewController.didSetupHockeySDK { + BITHockeyManager.shared().configure(withIdentifier: "APP_IDENTIFIER") + BITHockeyManager.shared().start() + TodayViewController.didSetupHockeySDK = true + } + } + } + ``` + +3. The binary distribution provides a special framework build in the `HockeySDKCrashOnly` or `HockeySDKCrashOnlyExtension` folder of the distribution zip file, which only contains crash reporting functionality (also automatic sending crash reports only). + + +### 3.5 WatchKit 1 Extensions + +The following points need to be considered to use HockeySDK with WatchKit 1 Extensions: + +1. WatchKit extensions don't use regular `UIViewControllers` but rather `WKInterfaceController` subclasses. These have a different lifecycle than you might be used to. + + To make sure that the HockeySDK is only instantiated once in the WatchKit extension's lifecycle we recommend using a helper class similar to this: + + **Objective-C** + + ```objc + @import Foundation; + + @interface BITWatchSDKSetup : NSObject + + * (void)setupHockeySDKIfNeeded; + + @end + ``` + + ```objc + #import "BITWatchSDKSetup.h" + @import HockeySDK + + static BOOL hockeySDKIsSetup = NO; + + @implementation BITWatchSDKSetup + + * (void)setupHockeySDKIfNeeded { + if (!hockeySDKIsSetup) { + [[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"APP_IDENTIFIER"]; + [[BITHockeyManager sharedHockeyManager] startManager]; + hockeySDKIsSetup = YES; + } + } + + @end + ``` + + **Swift** + + ```swift + import HockeySDK + + class BITWatchSDKSetup { + + static var hockeySDKIsSetup = false; + + static func setupHockeySDKIfNeeded() { + if !BITWatchSDKSetup.hockeySDKIsSetup { + BITHockeyManager.shared().configure(withIdentifier: "APP_IDENTIFIER") + BITHockeyManager.shared().start() + BITWatchSDKSetup.hockeySDKIsSetup = true; + } + } + } + ``` + + + Then, in each of your WKInterfaceControllers where you want to use the HockeySDK, you should do this: + + **Objective-C** + + ```objc + #import "InterfaceController.h" + @import HockeySDK + #import "BITWatchSDKSetup.h" + + @implementation InterfaceController + + + (void)awakeWithContext:(id)context { + [super awakeWithContext:context]; + [BITWatchSDKSetup setupHockeySDKIfNeeded]; + } + + + (void)willActivate { + [super willActivate]; + } + + + (void)didDeactivate { + [super didDeactivate]; + } + + @end + ``` + + **Swift** + + ```swift + class InterfaceController: WKInterfaceController { + + override func awake(withContext context: Any?) { + super.awake(withContext: context) + BITWatchSDKSetup.setupHockeySDKIfNeeded() + } + + override func willActivate() { + super.willActivate() + } + + override func didDeactivate() { + super.didDeactivate() + } + + } + ``` +2. The binary distribution provides a special framework build in the `HockeySDKCrashOnly` or `HockeySDKCrashOnlyExtension` folder of the distribution zip file, which only contains crash reporting functionality (also automatic sending crash reports only). + + +### 3.6 Crash Reporting + +The following options only show some of the possibilities to interact and fine-tune the crash reporting feature. For more please check the full documentation of the `BITCrashManager` class in our [documentation](#documentation). + +#### 3.6.1 Disable Crash Reporting +The HockeySDK enables crash reporting **per default**. Crashes will be immediately sent to the server the next time the app is launched. + +To provide you with the best crash reporting, we are using a build of [PLCrashReporter]("https://github.com/plausiblelabs/plcrashreporter") based on [Version 1.2.1 / Commit 356901d7f3ca3d46fbc8640f469304e2b755e461]("https://github.com/plausiblelabs/plcrashreporter/commit/356901d7f3ca3d46fbc8640f469304e2b755e461"). + +This feature can be disabled as follows: + +**Objective-C** + +```objc +[[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"APP_IDENTIFIER"]; + +[[BITHockeyManager sharedHockeyManager] setDisableCrashManager: YES]; //disable crash reporting + +[[BITHockeyManager sharedHockeyManager] startManager]; +``` + +**Swift** + +```swift +BITHockeyManager.shared().configure(withIdentifier: "APP_IDENTIFIER") +BITHockeyManager.shared().isCrashManagerDisabled = true +BITHockeyManager.shared().start() +``` +#### 3.6.2 Auto send crash reports + +Crashes are send the next time the app starts. If `crashManagerStatus` is set to `BITCrashManagerStatusAutoSend`, crashes will be send without any user interaction, otherwise an alert will appear allowing the users to decide whether they want to send the report or not. + +**Objective-C** + +```objc +[[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"APP_IDENTIFIER"]; + +[[BITHockeyManager sharedHockeyManager].crashManager setCrashManagerStatus: BITCrashManagerStatusAutoSend]; + +[[BITHockeyManager sharedHockeyManager] startManager]; +``` + +**Swift** + +```swift +BITHockeyManager.shared().configure(withIdentifier: "APP_IDENTIFIER") +BITHockeyManager.shared().crashManager.crashManagerStatus = BITCrashManagerStatus.autoSend +BITHockeyManager.shared().start() +``` + +The SDK is not sending the reports right when the crash happens deliberately, because if is not safe to implement such a mechanism while being async-safe (any Objective-C code is _NOT_ async-safe!) and not causing more danger like a deadlock of the device, than helping. We found that users do start the app again because most don't know what happened, and you will get by far most of the reports. + +Sending the reports on start-up is done asynchronously (non-blocking). This is the only safe way to ensure that the app won't be possibly killed by the iOS watchdog process, because start-up could take too long and the app could not react to any user input when network conditions are bad or connectivity might be very slow. + +#### 3.6.3 Mach Exception Handling + +By default the SDK is using the safe and proven in-process BSD Signals for catching crashes. This option provides an option to enable catching fatal signals via a Mach exception server instead. + +We strongly advise _NOT_ to enable Mach exception handler in release versions of your apps! + +*Warning:* The Mach exception handler executes in-process, and will interfere with debuggers when they attempt to suspend all active threads (which will include the Mach exception handler). Mach-based handling should _NOT_ be used when a debugger is attached. The SDK will not enable catching exceptions if the app is started with the debugger running. If you attach the debugger during runtime, this may cause issues the Mach exception handler is enabled! + +**Objective-C** + +```objc +[[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"APP_IDENTIFIER"]; + +[[BITHockeyManager sharedHockeyManager].crashManager setEnableMachExceptionHandler: YES]; + +[[BITHockeyManager sharedHockeyManager] startManager]; +``` + +**Swift** + +```swift +BITHockeyManager.shared().configure(withIdentifier: "APP_IDENTIFIER") +BITHockeyManager.shared().crashManager.isMachExceptionHandlerEnabled = true +BITHockeyManager.shared().start() +``` + +#### 3.6.4 Attach additional data + +The `BITHockeyManagerDelegate` protocol provides methods to add additional data to a crash report: + +1. UserID: + +**Objective-C** + +`- (NSString *)userIDForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager;` + +**Swift** + +`optional public func userID(for hockeyManager: BITHockeyManager!, componentManager: BITHockeyBaseManager!) -> String!` + +2. UserName: + +**Objective-C** + +`- (NSString *)userNameForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager;` + +**Swift** + +`optional public func userName(for hockeyManager: BITHockeyManager!, componentManager: BITHockeyBaseManager!) -> String!` + +3. UserEmail: + +**Objective-C** + +`- (NSString *)userEmailForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager;` + +**Swift** + +`optional public func userEmail(for hockeyManager: BITHockeyManager!, componentManager: BITHockeyBaseManager!) -> String!` + +The `BITCrashManagerDelegate` protocol (which is automatically included in `BITHockeyManagerDelegate`) provides methods to add more crash specific data to a crash report: + +1. Text attachments: + +**Objective-C** + +`-(NSString *)applicationLogForCrashManager:(BITCrashManager *)crashManager` + +**Swift** + +`optional public func applicationLog(for crashManager: BITCrashManager!) -> String!` + + Check the following tutorial for an example on how to add CocoaLumberjack log data: [How to Add Application Specific Log Data on iOS or OS X](http://support.hockeyapp.net/kb/client-integration-ios-mac-os-x/how-to-add-application-specific-log-data-on-ios-or-os-x) +2. Binary attachments: + +**Objective-C** + +`-(BITHockeyAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManager` + +**Swift** + +`optional public func attachment(for crashManager: BITCrashManager!) -> BITHockeyAttachment!` + +Make sure to implement the protocol + +**Objective-C** + +```objc +@interface YourAppDelegate () {} + +@end +``` + +**Swift** + +```swift +class YourAppDelegate: BITHockeyManagerDelegate { + +} +``` + +and set the delegate: + +**Objective-C** + +```objc +[[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"APP_IDENTIFIER"]; + +[[BITHockeyManager sharedHockeyManager] setDelegate: self]; + +[[BITHockeyManager sharedHockeyManager] startManager]; +``` + +**Swift** + +```swift +BITHockeyManager.shared().configure(withIdentifier: "APP_IDENTIFIER") +BITHockeyManager.shared().delegate = self +BITHockeyManager.shared().start() +``` + + +### 3.7 User Metrics + +HockeyApp automatically provides you with nice, intelligible, and informative metrics about how your app is used and by whom. + +- **Sessions**: A new session is tracked by the SDK whenever the containing app is restarted (this refers to a 'cold start', i.e., when the app has not already been in memory prior to being launched) or whenever it becomes active again after having been in the background for 20 seconds or more. +- **Users**: The SDK anonymously tracks the users of your app by creating a random UUID that is then securely stored in the iOS keychain. This anonymous ID is stored in the keychain, as of iOS 10, it no longer persists across re-installations. +- **Custom Events**: With HockeySDK 4.1.0 and later, you can now track Custom Events in your app, understand user actions and see the aggregates on the HockeyApp portal. +- **Batching & offline behavior**: The SDK batches up to 50 events or waits for 15s and then persist and send the events, whichever comes first. So for sessions, this might actually mean we send 1 single event per batch. If you are sending Custom Events, it can be 1 session event plus X of your Custom Events (up to 50 events per batch total). In case the device is offline, up to 300 events are stored until the SDK starts to drop new events. + +Just in case you want to opt-out of the automatic collection of anonymous users and sessions statistics, there is a way to turn this functionality off at any time: + +**Objective-C** + +```objc +[BITHockeyManager sharedHockeyManager].disableMetricsManager = YES; +``` + +**Swift** + +```swift +BITHockeyManager.shared().isMetricsManagerDisabled = true +``` + +#### 3.7.1 Custom Events + +By tracking custom events, you can now get insight into how your customers use your app, understand their behavior and answer important business or user experience questions while improving your app. + +- Before starting to track events, ask yourself the questions that you want to get answers to. For instance, you might be interested in business, performance/quality or user experience aspects. +- Name your events in a meaningful way and keep in mind that you will use these names when searching for events in the HockeyApp web portal. It is your responsibility to not collect personal information as part of the events tracking. + +**Objective-C** + +```objc +BITMetricsManager *metricsManager = [BITHockeyManager sharedHockeyManager].metricsManager; + +[metricsManager trackEventWithName:eventName] +``` + +**Swift** + +```swift +let metricsManager = BITHockeyManager.shared().metricsManager + +metricsManager.trackEvent(withName: eventName) +``` + +**Limitations** + +- Accepted characters for tracking events are: [a-zA-Z0-9_. -]. If you use other than the accepted characters, your events will not show up in the HockeyApp web portal. +- There is currently a limit of 300 unique event names per app per week. +- There is _no_ limit on the number of times an event can happen. + +#### 3.7.2 Attaching custom properties and measurements to a custom event + +It's possible to attach properties and/or measurements to a custom event. There is one limitation to attaching properties and measurements. They currently don't show up in the HockeyApp dashboard but you have to link your app to Application Insights to be able to query them. Please have a look at [our blog post](https://www.hockeyapp.net/blog/2016/08/30/custom-events-public-preview.html) to find out how to do that. + +- Properties have to be a string. +- Measurements have to be of a numeric type. + +**Objective-C** + +```objc +BITMetricsManager *metricsManager = [BITHockeyManager sharedHockeyManager].metricsManager; + +NSDictionary *myProperties = @{@"Property 1" : @"Something", + @"Property 2" : @"Other thing", + @"Property 3" : @"Totally different thing"}; +NSDictionary *myMeasurements = @{@"Measurement 1" : @1, + @"Measurement 2" : @2.34, + @"Measurement 3" : @2000000}; + +[metricsManager trackEventWithName:eventName properties:myProperties measurements:myMeasurements] +``` + +**Swift** + +```swift +let myProperties = ["Property 1": "Something", "Property 2": "Other thing", "Property 3" : "Totally different thing."] +let myMeasurements = ["Measurement 1": 1, "Measurement 2": 2.3, "Measurement 3" : 30000] + +let metricsManager = BITHockeyManager.shared().metricsManager +metricsManager.trackEvent(withName: eventName, properties: myProperties, measurements: myMeasurements) +``` + + +### 3.8 Feedback + +As of HockeySDK 4.1.1, Feedback is no longer part of the default SDK. To use feedback in your app, integrate the SDK with all features as follows: + +#### 3.8.1 Integrate the full-featured SDK. + +If you're integrating the binary yourself, use the `HockeySDK.embeddedframework` in the subfolder `HockeySDKAllFeatures`. If you're using CocoaPods, use + +```ruby +pod "HockeySDK", :subspecs => ['AllFeaturesLib'] +``` + +in your podfile. + +`BITFeedbackManager` lets your users communicate directly with you via the app and an integrated user interface. It provides a single threaded discussion with a user running your app. This feature is only enabled if you integrate the actual view controllers into your app. + +You should never create your own instance of `BITFeedbackManager` but use the one provided by the `[BITHockeyManager sharedHockeyManager]`: + +**Objective-C** + +```objc +[BITHockeyManager sharedHockeyManager].feedbackManager +``` + +**Swift** + +```swift +BITHockeyManager.shared().feedbackManager +``` + +Please check the [documentation](#documentation) of the `BITFeedbackManager` and `BITFeedbackManagerDelegate` classes on more information on how to leverage this feature. + +#### 3.8.2 Add the NSPhotoLibraryUsageDescription to your Info.plist. + +As of iOS 10, developers have to add UsageDescription-strings before using system frameworks with privacy features (read more on this in [Apple's own documentation](https://developer.apple.com/library/prerelease/content/releasenotes/General/WhatsNewIniOS/Articles/iOS10.html#//apple_ref/doc/uid/TP40017084-SW3)). To make allow users to attach photos to feedback, add the `NSPhotoLibraryUsageDescription` to your `Info.plist` and provide a description. Make sure to localize your description as described in [Apple's documentation about localizing Info.plist strings](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/AboutInformationPropertyListFiles.html). + +If the value is missing from your `Info.plist`, the SDK will disable attaching photos to feedback and disable the creation of a new feedback item in case of a screenshot. + + + +### 3.9 Store Updates + +This is the HockeySDK module for handling app updates when having your app released in the App Store. + +When an update is detected, this module will show an alert asking the user if he/she wants to update or ignore this version. If the update was chosen, it will open the apps page in the app store app. + +By default this module is **NOT** enabled! To enable it use the following code: + +**Objective-C** + +```objc +[[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"APP_IDENTIFIER"]; + +[[BITHockeyManager sharedHockeyManager] setEnableStoreUpdateManager: YES]; + +[[BITHockeyManager sharedHockeyManager] startManager]; +``` + +**Swift** + +```swift +BITHockeyManager.shared().configure(withIdentifier: "APP_IDENTIFIER") +BITHockeyManager.shared().isStoreUpdateManagerEnabled = true +BITHockeyManager.shared().start() +``` + +When this module is enabled and **NOT** running in an App Store build/environment, it won't do any checks! + +Please check the [documentation](#documentation) of the `BITStoreUpdateManager` class on more information on how to leverage this feature and know about its limits. + + +### 3.10 In-App-Updates (Beta & Enterprise only) + +The following options only show some of the possibilities to interact and fine-tune the update feature when using Ad-Hoc or Enterprise provisioning profiles. For more please check the full documentation of the `BITUpdateManager` class in our [documentation](#documentation). + +The feature handles version updates, presents the update and version information in a App Store like user interface, collects usage information and provides additional authorization options when using Ad-Hoc provisioning profiles. + +This module automatically disables itself when running in an App Store build by default! + +This feature can be disabled manually as follows: + +**Objective-C** + +```objc +[[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"APP_IDENTIFIER"]; + +[[BITHockeyManager sharedHockeyManager] setDisableUpdateManager: YES]; //disable auto updating + +[[BITHockeyManager sharedHockeyManager] startManager]; +``` + +**Swift** + +```swift +BITHockeyManager.shared().configure(withIdentifier: "APP_IDENTIFIER") +BITHockeyManager.shared().isUpdateManagerDisabled = true +BITHockeyManager.shared().start() +``` + +Please note that the SDK expects your CFBundleVersion values to always increase and never reset to detect a new update. + +If you want to see beta analytics, use the beta distribution feature with in-app updates, restrict versions to specific users, or want to know who is actually testing your app, you need to follow the instructions on our guide [Authenticating Users on iOS](http://support.hockeyapp.net/kb/client-integration-ios-mac-os-x/authenticating-users-on-ios) + + +### 3.11 Debug information + +To check if data is send properly to HockeyApp and also see some additional SDK debug log data in the console, add the following line before `startManager`: + +```objc +[[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"APP_IDENTIFIER"]; + +[BITHockeyManager sharedHockeyManager].logLevel = BITLogLevelDebug; + +[[BITHockeyManager sharedHockeyManager] startManager]; +``` + + +## 4. Documentation + +Our documentation can be found on [HockeyApp](http://hockeyapp.net/help/sdk/ios/5.1.2/index.html). + + +## 5.Troubleshooting + +### Linker warnings + + Make sure that all mentioned frameworks and libraries are linked + +### iTunes Connect rejection + + Make sure none of the following files are copied into your app bundle, check under app target, `Build Phases`, `Copy Bundle Resources` or in the `.app` bundle after building: + + - `HockeySDK.framework` (except if you build a dynamic framework version of the SDK yourself!) + - `de.bitstadium.HockeySDK-iOS-5.1.2.docset` + +### Features are not working as expected + + Enable debug output to the console to see additional information from the SDK initializing the modules, sending and receiving network requests and more by adding the following code before calling `startManager`: + + `[BITHockeyManager sharedHockeyManager].logLevel = BITLogLevelDebug;` + +### Wrong strings or "Missing HockeySDKResources.bundle" error + +1. Please check if the `HockeySDKResources.bundle` is added to your app bundle. Use Finder to inspect your `.app` bundle to see if the bundle is added. + +2. If it is missing, please check if the resources bundle is mentioned in your app target's `Copy Bundle Resources` build step in the `Build Phases` tab. Add the resource bundle manually if necessary. + +3. Make a clean build and try again. + +![Screenshot_2015-12-22_01.07.27.png](https://support.hockeyapp.net/help/assets/0e9d2eb58de8355363b89bd491d6fcf4c14f596e/normal/Screenshot_2015-12-22_01.07.27.png) + + +## 6. Contributing + +We're looking forward to your contributions via pull requests on our [GitHub repository](https://github.com/bitstadium/HockeySDK-iOS). + + +### 6.1 Development environment + +* A Mac running the latest version of macOS. +* Get the latest Xcode from the Mac App Store. +* [Jazzy](https://github.com/realm/jazzy) to generate documentation. +* [CocoaPods](https://cocoapods.org/) to test integration with CocoaPods. +* [Carthage](https://github.com/Carthage/Carthage) to test integration with Carthage. + + +### 6.2 Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + + +### 6.3 Contributor License + +You must sign a [Contributor License Agreement](https://cla.microsoft.com/) before submitting your pull request. To complete the Contributor License Agreement (CLA), you will need to submit a request via the [form](https://cla.microsoft.com/) and then electronically sign the CLA when you receive the email containing the link to the document. You need to sign the CLA only once to cover submission to any Microsoft OSS project. + + +## 7. Contact + +If you have further questions or are running into trouble that cannot be resolved by any of the steps here, feel free to open [a GitHub issue](https://github.com/bitstadium/HockeySDK-iOS/issues), contact us at [support@hockeyapp.net](mailto:support@hockeyapp.net) or join our [Slack](https://slack.hockeyapp.net). diff --git a/submodules/HockeySDK-iOS/Documentation/Guides/Migration Kits.md b/submodules/HockeySDK-iOS/Documentation/Guides/Migration Kits.md new file mode 100644 index 0000000000..d8e474e936 --- /dev/null +++ b/submodules/HockeySDK-iOS/Documentation/Guides/Migration Kits.md @@ -0,0 +1,189 @@ +## Introduction + +This guide will help you migrate from QuincyKit, HockeyKit or an older version of HockeySDK-iOS to the latest release of the unified HockeySDK for iOS. + +First of all we will cleanup the obsolete installation files and then convert your existing code to the new API calls. + +## Cleanup + +First of all you should remove all files from prior versions of either QuincyKit, HockeyKit or HockeySDK-iOS. If you not sure which files you added, here are a few easy steps for each SDK. + +### QuincyKit + +In Xcode open the `Project Navigator` (⌘+1). In the search field at the bottom enter "Quincy". QuincyKit is installed, if search finds the following files: + +* BWQuincyManager.h +* BWQuincyManager.m +* Quincy.bundle + +Delete them all ("Move to Trash"). Or if you have them grouped into a folder (for example Vendor/QuincyKit) delete the folder. + +### HockeyKit + +In Xcode open the `Project Navigator` (⌘+1). In the search field at the bottom enter "Hockey". HockeyKit is installed, if search finds for example: + +* BWHockeyManager.h +* Hockey.bundle + +All of them should be in one folder/group in Xcode. Remove that folder. + +### HockeySDK-iOS before v2.5 + +In Xcode open the `Project Navigator` (⌘+1). In the search field at the bottom enter "CNSHockeyManager". If search returns any results you have the first release of our unified SDK added to your project. Even if you added it as a git submodule we would suggest you remove it first. + +### HockeySDK-iOS v2.5.x + +In Xcode open the `Project Navigator` (⌘+1). In the search field at the bottom enter `HockeySDK.framework`. If search returns any results you have the first release of our unified SDK added to your project. Even if you added it as a git submodule we would suggest you remove it first. Repeat the same for `CrashReporter.framework` and `HockeySDKResources.bundle`. + +### HockeySDK-iOS v3.0.x + +In Xcode open the `Project Navigator` (⌘+1). In the search field at the bottom enter `HockeySDK.embeddedFramework`. If search returns any results you have the first release of our unified SDK added to your project. Even if you added it as a git submodule we would suggest you remove it first. + +### Final Steps + +Search again in the `Project Navigator` (⌘+1) for "CrashReporter.framework". You shouldn't get any results now. If not, remove the CrashReporter.framework from your project. + +## Installation + +Follow the steps in our installation guide for either [Installation with binary framework distribution](http://support.hockeyapp.net/kb/client-integration/hockeyapp-for-ios-hockeysdk#framework) (Recommended) or [Installation as a subproject](http://support.hockeyapp.net/kb/client-integration/hockeyapp-for-ios-hockeysdk#subproject) + +After you finished the steps for either of the installation procedures, we have to migrate your existing code. + +## Setup + +### QuincyKit / HockeyKit + +In your application delegate (for example `AppDelegate.m`) search for the following lines: + + ```objc + [[BWQuincyManager sharedQuincyManager] setAppIdentifier:@"0123456789abcdef"]; + + [[BWHockeyManager sharedHockeyManager] setAppIdentifier:@"0123456789abcdef"]; + [[BWHockeyManager sharedHockeyManager] setUpdateURL:@"https://rink.hockeyapp.net/"]; + ``` + +If you use (as recommended) different identifiers for beta and store distribution some lines may be wrapped with compiler macros like this: + + ```objc + #if defined (CONFIGURATION_Beta) + [[BWQuincyManager sharedQuincyManager] setAppIdentifier:@"BETA_IDENTIFIER"]; + #endif + + #if defined (CONFIGURATION_Distribution) + [[BWQuincyManager sharedQuincyManager] setAppIdentifier:@"LIVE_IDENTIFIER"]; + #endif + ``` + +For now comment out all lines with either `[BWQuincyManager sharedQuincyManager]` or `[BWHockeyManager sharedHockeyManager]`. + +Open the header file of your application delegate (for example `AppDelegate.m`) or just press ^ + ⌘ + ↑ there should be a line like this (AppDelegate should match the name of the file) + + ```objc + @interface AppDelegate : NSObject { + ``` + +Remove the `BWHockeyManagerDelegate`. Also look for the following line: + + ```objc + #import "BWHockeyManager.h" + ``` + +And remove it too. (This line may have a #if macro around it, remove that too) + +Now follow the steps described in our [setup guide](http://support.hockeyapp.net/kb/client-integration/hockeyapp-for-ios-hockeysdk#setup) The values for `LIVE_IDENTIFIER` and `BETA_IDENTIFIER` are used in the setup guide. + +After you have finished the setup guide make sure everything works as expected and then delete the out commented lines from above. + +### HockeySDK-iOS before 2.5 + +In your application delegate (for example `AppDelegate.m`) search for the following lines: + + ```objc + [[CNSHockeyManager sharedHockeyManager] configureWithBetaIdentifier:BETA_IDENTIFIER + liveIdentifier:LIVE_IDENTIFIER + delegate:self]; + ``` + +For now comment out all lines with `[CNSHockeyManager sharedHockeyManager]`. Open the header file of your application delegate by pressing ^ + ⌘ + ↑. There should be a line like this: + + ```objc + @interface AppDelegate : NSObject { + ``` + +Remove `CNSHockeyManagerDelegate`, also look for this line: + + ```objc + #import "CNSHockeyManager.h" + ``` + +And remove that too. + +Now follow the steps described in our [setup guide](http://support.hockeyapp.net/kb/client-integration/hockeyapp-for-ios-hockeysdk#setup) The values for `LIVE_IDENTIFIER` and `BETA_IDENTIFIER` are used in the setup guide. + +After you have finished the setup guide make sure everything works as expected and then delete the out commented lines from above. + +### HockeySDK-iOS 2.5.x + +There are no changes to the SDK setup code required. Some delegates methods are deprecated and should be replaced as soon as feasible. + +The following delegates in `BITCrashManagerDelegate` moved to `BITHockeyManagerDelegate`: + +- `- (NSString *)userNameForCrashManager:(BITCrashManager *)crashManager;` is now `- (NSString *)userNameForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager;` +- `- (NSString *)userEmailForCrashManager:(BITCrashManager *)crashManager;` is now `- (NSString *)userEmailForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager;` + +### HockeySDK-iOS 3.0.x + +Instead of implementing the individual protocols in your app delegate, you can now simply add `BITHockeyManagerDelegate` alone, e.g.: + + ```objc + @interface BITAppDelegate () {} + + @end + ``` + +The delegate `-(NSString *)customDeviceIdentifierForUpdateManager:(BITUpdateManager *)updateManager` has been removed. To identify the installation please use the new `BITAuthenticator` class. + +### HockeySDK-iOS 3.5.x + +If you are using `PLCrashReporterCallbacks`, you now have to use `BITCrashManagerCallbacks` instead. This `struct` doesn't contain `version` any longer, so you have to remove that. Otherwise everything is the same. + +If you did set the delegate per component, e.g. `[[BITHockeyManager sharedHockeyManager].crashManager setDelegate:self]`, you need to remove these and set the delegate this way only: `[[BITHockeyManager sharedHockeyManager] setDelegate:self]`. This will propagate the delegate to all SDK components. Make sure to set it before calling `startManager`! + +In addition you need to make sure all of these frameworks are linked: + +- `AssetsLibrary` +- `CoreText` +- `CoreGraphics` +- `Foundation` +- `MobileCoreServices` +- `QuartzCore` +- `QuickLook` +- `Security` +- `SystemConfiguration` +- `UIKit` + +### HockeySDK-iOS 3.7.x + +You need to make sure all of these frameworks are linked: + +- `AssetsLibrary` +- `CoreText` +- `CoreGraphics` +- `Foundation` +- `MobileCoreServices` +- `QuartzCore` +- `QuickLook` +- `Security` +- `SystemConfiguration` +- `UIKit` +- `libc++` + +## Troubleshooting + +### ld: warning: directory not found for option '....QuincyKit.....' + +This warning means there is still a `Framework Search Path` pointing to the folder of the old SDK. Open the `Project Navigator` (⌘+1) and go to the tab `Build Settings`. In the search field enter the name of the folder mentioned in the warning (for example "QuincyKit") . If the search finds something in `Framework Search Paths` you should double click that entry and remove the line which points to the old folder. + +## Advanced Migration + +If you used any optional API calls, for example adding a custom description to a crash report, migrating those would exceed the scope of this guide. Please have a look at the [API documentation](http://hockeyapp.net/releases/). diff --git a/submodules/HockeySDK-iOS/Documentation/Guides/Set Custom AlertViewHandler.md b/submodules/HockeySDK-iOS/Documentation/Guides/Set Custom AlertViewHandler.md new file mode 100644 index 0000000000..b11ea780ea --- /dev/null +++ b/submodules/HockeySDK-iOS/Documentation/Guides/Set Custom AlertViewHandler.md @@ -0,0 +1,133 @@ +## Introduction + +HockeySDK lets the user decide wether to send a crash report or lets the developer send crash reports automatically without user interaction. In addition it is possible to attach more data like logs, a binary, or the users name, email or a user ID if this is already known. + +Starting with HockeySDK version 3.6 it is possible to customize this even further and implement your own flow to e.g. ask the user for more details about what happened or his name and email address if your app doesn't know that yet. + +The following example shows how this could be implemented. We'll present a custom UIAlertView asking the user for more details and attaching that to the crash report. + +## HowTo + +1. Setup the SDK +2. Configure HockeySDK to use your custom alertview handler using the `[[BITHockeyManager sharedHockeyManager].crashManager setAlertViewHandler:(BITCustomAlertViewHandler)alertViewHandler;` method in your AppDelegate. +3. Implement your handler in a way that it calls `[[BITHockeyManager sharedHockeyManager].crashManagerhandleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData]` with the input provided by the user. +4. Dismiss your custom view. + +## Example + +**Objective-C** + +```objc +@interface BITAppDelegate () +@end + + +@implementation BITAppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [self.window makeKeyAndVisible]; + + [[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"<>" + delegate:nil]; + + // optionally enable logging to get more information about states. + [BITHockeyManager sharedHockeyManager].debugLogEnabled = YES; + + [[BITHockeyManager sharedHockeyManager].crashManager setAlertViewHandler:^(){ + NSString *exceptionReason = [[BITHockeyManager sharedHockeyManager].crashManager lastSessionCrashDetails].exceptionReason; + UIAlertView *customAlertView = [[UIAlertView alloc] initWithTitle: @"Oh no! The App crashed" + message: nil + delegate: self + cancelButtonTitle: @"Don't send" + otherButtonTitles: @"Send", @"Always send", nil]; + if (exceptionReason) { + customAlertView.message = @"We would like to send a crash report to the developers. Please enter a short description of what happened:"; + customAlertView.alertViewStyle = UIAlertViewStylePlainTextInput; + } else { + customAlertView.message = @"We would like to send a crash report to the developers"; + } + + [customAlertView show]; + }]; + + [[BITHockeyManager sharedHockeyManager].authenticator authenticateInstallation]; + + return YES; +} + +- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { + BITCrashMetaData *crashMetaData = [BITCrashMetaData new]; + if (alertView.alertViewStyle != UIAlertViewStyleDefault) { + crashMetaData.userDescription = [alertView textFieldAtIndex:0].text; + } + switch (buttonIndex) { + case 0: + [[BITHockeyManager sharedHockeyManager].crashManager handleUserInput:BITCrashManagerUserInputDontSend withUserProvidedMetaData:nil]; + break; + case 1: + [[BITHockeyManager sharedHockeyManager].crashManager handleUserInput:BITCrashManagerUserInputSend withUserProvidedMetaData:crashMetaData]; + break; + case 2: + [[BITHockeyManager sharedHockeyManager].crashManager handleUserInput:BITCrashManagerUserInputAlwaysSend withUserProvidedMetaData:crashMetaData]; + break; + } +} + +@end +``` + +**Swift** + +```swift +import UIKit +import HockeySDK + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate, UIAlertViewDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + window?.makeKeyAndVisible() + + BITHockeyManager.shared().configure(withIdentifier: "APP_IDENTIFIER") + // optionally enable logging to get more information about states. + BITHockeyManager.shared().logLevel = BITLogLevel.verbose + + BITHockeyManager.shared().crashManager.setAlertViewHandler { + let exceptionReason = BITHockeyManager.shared().crashManager.lastSessionCrashDetails.exceptionReason + let customAlertView = UIAlertView.init(title: "Oh no! The App crashed", + message: "The App crashed", + delegate: self, + cancelButtonTitle: "Don't send", + otherButtonTitles: "Send", "Always send") + if (exceptionReason != nil) { + customAlertView.message = "We would like to send a crash report to the developers. Please enter a short description of what happened:" + customAlertView.alertViewStyle = UIAlertViewStyle.plainTextInput; + } else { + customAlertView.message = "We would like to send a crash report to the developers" + } + customAlertView.show() + } + + return true + } + + func alertView(_ alertView: UIAlertView, didDismissWithButtonIndex buttonIndex: Int) { + let crashMetaData = BITCrashMetaData(); + if (alertView.alertViewStyle != UIAlertViewStyle.default) { + crashMetaData.userProvidedDescription = alertView.textField(at: 0)?.text + } + switch (buttonIndex) { + case 0: + BITHockeyManager.shared().crashManager.handle(BITCrashManagerUserInput.dontSend, withUserProvidedMetaData: nil) + case 1: + BITHockeyManager.shared().crashManager.handle(BITCrashManagerUserInput.send, withUserProvidedMetaData: crashMetaData) + case 2: + BITHockeyManager.shared().crashManager.handle(BITCrashManagerUserInput.alwaysSend, withUserProvidedMetaData: crashMetaData) + } + } +} +``` + + diff --git a/submodules/HockeySDK-iOS/Documentation/Guides/Upload Symbols.md b/submodules/HockeySDK-iOS/Documentation/Guides/Upload Symbols.md new file mode 100644 index 0000000000..8b474a0710 --- /dev/null +++ b/submodules/HockeySDK-iOS/Documentation/Guides/Upload Symbols.md @@ -0,0 +1,25 @@ +## Introduction + +Mac and iOS crash reports show the stack traces for all running threads of your app of the time a crash occurred. But the stack traces only contain memory addresses and don't show class names, methods, file names and line numbers that are needed to understand them. + +To get these memory addresses translated you need to upload a dSYM package to the server, which contains all information required to make this happen. The symbolication process will then check the binary images section of the crash report and grab the UUID of the binary that caused the crash. Next it will get the UUID of the dSYM package to make sure they are identical and process the data if so. + +**WARNING:** Every time you are doing a build, the app binary and the dSYM will get a new unique UUID, no matter if you changed the code or not. So make sure to archive all your binaries and dSYMs that you are using for beta or app store builds! +This will also apply when using Bitcode. Then, Apple will use your uploaded build and re-compile it on their end. Whenever this happens, this also changes the UUID and requires you to download the newly generated dSYM from Apple and upload it to HockeyApp. + +## HowTo + +Once you have your app ready for beta testing or even to submit it to the App Store, you need to upload the `.dSYM` bundle to HockeyApp to enable symbolication. If you have built your app with Xcode, menu `Product` > `Archive`, you can find the `.dSYM` as follows: + +1. Chose `Window` > `Organizer` in Xcode. +2. Select the tab Archives. +3. Select your app in the left sidebar. +4. Right-click on the latest archive and select `Show in Finder`. +5. Right-click the `.xcarchive` in Finder and select `Show Package Contents`. +6. You should see a folder named dSYMs which contains your dSYM bundle. If you use Safari, just drag this file from Finder and drop it on to the corresponding drop zone in HockeyApp. If you use another browser, copy the file to a different location, then right-click it and choose Compress `YourApp.dSYM`. The file will be compressed as a .zip file. Drag & drop this file to HockeyApp. + +## Mac Desktop Uploader + +As an alternative, you can use our [HockeyApp for Mac](http://hockeyapp.net/releases/mac/) app to upload the complete archive in one step. + +Also check out the guide on [how to upload to HockeyApp from Mac OS X](http://support.hockeyapp.net/kb/client-integration-ios-mac-os-x/how-to-upload-to-hockeyapp-from-mac-os-x). \ No newline at end of file diff --git a/submodules/HockeySDK-iOS/Documentation/HockeySDK/.jazzy.yaml b/submodules/HockeySDK-iOS/Documentation/HockeySDK/.jazzy.yaml new file mode 100644 index 0000000000..5e5c34f9c8 --- /dev/null +++ b/submodules/HockeySDK-iOS/Documentation/HockeySDK/.jazzy.yaml @@ -0,0 +1,39 @@ +objc: true +clean: true +sdk: iphonesimulator + +theme: ../Themes/apple + +module: HockeySDK +module_version: 5.1.2 +author: Microsoft Corp +author_url: https://www.microsoft.com + +readme: ../../README.md +documentation: ../Guides/*.md +custom_categories: + - name: Guides + children: + - Installation & Setup + - Migration Kits + - name: How To + children: + - App Versioning + - Set Custom AlertViewHandler + - Upload Symbols + - name: Troubleshooting + children: + - Crash Reporting Not Working + - name: Release Notes + children: + - Changelog + +umbrella_header: ../../Classes/HockeySDK.h + +root_url: https://support.hockeyapp.net/kb/api +github_url: https://github.com/bitstadium/HockeySDK-iOS/ +github_file_prefix: "https://github.com/bitstadium/HockeySDK-iOS/" + +skip_undocumented: true + +output: Generated/ diff --git a/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/css/highlight.css.scss b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/css/highlight.css.scss new file mode 100755 index 0000000000..7bc1f2911f --- /dev/null +++ b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/css/highlight.css.scss @@ -0,0 +1,63 @@ +/* Credit to https://gist.github.com/wataru420/2048287 */ + +.highlight { + .c { color: #999988; font-style: italic } /* Comment */ + .err { color: #a61717; background-color: #e3d2d2 } /* Error */ + .k { color: #000000; font-weight: bold } /* Keyword */ + .o { color: #000000; font-weight: bold } /* Operator */ + .cm { color: #999988; font-style: italic } /* Comment.Multiline */ + .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ + .c1 { color: #999988; font-style: italic } /* Comment.Single */ + .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ + .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ + .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ + .ge { color: #000000; font-style: italic } /* Generic.Emph */ + .gr { color: #aa0000 } /* Generic.Error */ + .gh { color: #999999 } /* Generic.Heading */ + .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ + .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ + .go { color: #888888 } /* Generic.Output */ + .gp { color: #555555 } /* Generic.Prompt */ + .gs { font-weight: bold } /* Generic.Strong */ + .gu { color: #aaaaaa } /* Generic.Subheading */ + .gt { color: #aa0000 } /* Generic.Traceback */ + .kc { color: #000000; font-weight: bold } /* Keyword.Constant */ + .kd { color: #000000; font-weight: bold } /* Keyword.Declaration */ + .kp { color: #000000; font-weight: bold } /* Keyword.Pseudo */ + .kr { color: #000000; font-weight: bold } /* Keyword.Reserved */ + .kt { color: #445588; } /* Keyword.Type */ + .m { color: #009999 } /* Literal.Number */ + .s { color: #d14 } /* Literal.String */ + .na { color: #008080 } /* Name.Attribute */ + .nb { color: #0086B3 } /* Name.Builtin */ + .nc { color: #445588; font-weight: bold } /* Name.Class */ + .no { color: #008080 } /* Name.Constant */ + .ni { color: #800080 } /* Name.Entity */ + .ne { color: #990000; font-weight: bold } /* Name.Exception */ + .nf { color: #990000; } /* Name.Function */ + .nn { color: #555555 } /* Name.Namespace */ + .nt { color: #000080 } /* Name.Tag */ + .nv { color: #008080 } /* Name.Variable */ + .ow { color: #000000; font-weight: bold } /* Operator.Word */ + .w { color: #bbbbbb } /* Text.Whitespace */ + .mf { color: #009999 } /* Literal.Number.Float */ + .mh { color: #009999 } /* Literal.Number.Hex */ + .mi { color: #009999 } /* Literal.Number.Integer */ + .mo { color: #009999 } /* Literal.Number.Oct */ + .sb { color: #d14 } /* Literal.String.Backtick */ + .sc { color: #d14 } /* Literal.String.Char */ + .sd { color: #d14 } /* Literal.String.Doc */ + .s2 { color: #d14 } /* Literal.String.Double */ + .se { color: #d14 } /* Literal.String.Escape */ + .sh { color: #d14 } /* Literal.String.Heredoc */ + .si { color: #d14 } /* Literal.String.Interpol */ + .sx { color: #d14 } /* Literal.String.Other */ + .sr { color: #009926 } /* Literal.String.Regex */ + .s1 { color: #d14 } /* Literal.String.Single */ + .ss { color: #990073 } /* Literal.String.Symbol */ + .bp { color: #999999 } /* Name.Builtin.Pseudo */ + .vc { color: #008080 } /* Name.Variable.Class */ + .vg { color: #008080 } /* Name.Variable.Global */ + .vi { color: #008080 } /* Name.Variable.Instance */ + .il { color: #009999 } /* Literal.Number.Integer.Long */ +} diff --git a/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/css/jazzy.css.scss b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/css/jazzy.css.scss new file mode 100755 index 0000000000..2cc89840ea --- /dev/null +++ b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/css/jazzy.css.scss @@ -0,0 +1,480 @@ +//////////////////////////////// +// Constants +//////////////////////////////// + +$bg_color: #414141; +$doc_coverage_color: #999; +$code_color: #777; +$code_bg_color: #eee; +$link_color: #0088cc; +$white_color: #fff; +$light_gray_bg_color: #f2f2f2; +$declaration_bg_color: #f9f9f9; +$sidebar_bg_color: #f9f9f9; +$declaration_title_language_color: #4b8afb; + +$sidebar_width: 230px; +$content_wrapper_width: 980px; +$content_top_offset: 70px; +$content_body_margin: 16px; +$content_body_left_offset: $sidebar_width + $content_body_margin; +$header_height: 26px; +$breadcrumb_padding_top: 17px; + +$code_font: 0.95em Menlo, monospace; + +$gray_border: 1px solid #e2e2e2; +$declaration_language_border: 5px solid #cde9f4; + +$aside_color: #aaa; +$aside_border: 5px solid lighten($aside_color, 20%); +$aside_warning_color: #ff0000; +$aside_warning_border: 5px solid lighten($aside_warning_color, 20%); + +//////////////////////////////// +// Reset +//////////////////////////////// + +html, body, div, span, h1, h3, h4, p, a, code, em, img, ul, li, table, tbody, tr, td { + background: transparent; + border: 0; + margin: 0; + outline: 0; + padding: 0; + vertical-align: baseline; +} + +//////////////////////////////// +// Global +//////////////////////////////// + +body { + background-color: $light_gray_bg_color; + font-family: Helvetica, freesans, Arial, sans-serif; + font-size: 14px; + -webkit-font-smoothing: subpixel-antialiased; + word-wrap: break-word; +} + +// Headers + +h1, h2, h3 { + margin-top: 0.8em; + margin-bottom: 0.3em; + font-weight: 100; + color: black; +} +h1 { + font-size: 2.5em; +} +h2 { + font-size: 2em; + border-bottom: $gray_border; +} +h4 { + font-size: 13px; + line-height: 1.5; + margin-top: 21px; +} +h5 { + font-size: 1.1em; +} +h6 { + font-size: 1.1em; + color: $code_color; +} +.section-name { + color: rgba(128,128,128,1); + display: block; + font-family: Helvetica; + font-size: 22px; + font-weight: 100; + margin-bottom: 15px; +} + +// Code + +pre, code { + font: $code_font; + color: $code_color; + word-wrap: normal; +} +p code, li code { + background-color: $code_bg_color; + padding: 2px 4px; + border-radius: 4px; +} + +// Links + +a { + color: $link_color; + text-decoration: none; +} + +// Lists + +ul { + padding-left: 15px; +} +li { + line-height: 1.8em; +} + +// Images + +img { + max-width: 100%; +} + +// Blockquotes + +blockquote { + margin-left: 0; + padding: 0 10px; + border-left: 4px solid #ccc; +} + +// General Content Wrapper + +.content-wrapper { + margin: 0 auto; + width: $content_wrapper_width; +} + +//////////////////////////////// +// Header & Top Breadcrumbs +//////////////////////////////// + +header { + font-size: 0.85em; + line-height: $header_height; + background-color: $bg_color; + position: fixed; + width: 100%; + z-index: 1; + img { + padding-right: 6px; + vertical-align: -4px; + height: 16px; + } + a { + color: $white_color; + } + p { + float: left; + color: $doc_coverage_color; + } + .header-right { + float: right; + margin-left: 16px; + } +} + +#breadcrumbs { + background-color: $light_gray_bg_color; + height: $content_top_offset - $header_height - $breadcrumb_padding_top; + padding-top: $breadcrumb_padding_top; + position: fixed; + width: 100%; + z-index: 1; + margin-top: $header_height; + #carat { + height: 10px; + margin: 0 5px; + } +} + +//////////////////////////////// +// Side Navigation +//////////////////////////////// + +.sidebar { + background-color: $sidebar_bg_color; + border: $gray_border; + overflow-y: auto; + overflow-x: hidden; + position: fixed; + top: $content_top_offset; + bottom: 0; + width: $sidebar_width; + word-wrap: normal; +} + +.nav-groups { + list-style-type: none; + background: $white_color; + padding-left: 0; +} + +.nav-group-name { + border-bottom: $gray_border; + font-size: 1.1em; + font-weight: 100; + padding: 15px 0 15px 20px; + > a { + color: #333; + } +} + +.nav-group-tasks { + margin-top: 5px; +} + +.nav-group-task { + font-size: 0.9em; + list-style-type: none; + white-space: nowrap; + a { + color: #888; + } +} + +//////////////////////////////// +// Main Content +//////////////////////////////// + +.main-content { + background-color: $white_color; + border: $gray_border; + margin-left: $content_body_left_offset; + position: absolute; + overflow: hidden; + padding-bottom: 60px; + top: $content_top_offset; + width: $content_wrapper_width - $content_body_left_offset; + p, a, code, em, ul, table, blockquote { + margin-bottom: 1em; + } + p { + line-height: 1.8em; + } + section { + .section:first-child { + margin-top: 0; + padding-top: 0; + } + + .task-group-section .task-group:first-of-type { + padding-top: 10px; + + .section-name { + padding-top: 15px; + } + } + } +} + +.section { + padding: 0 25px; +} + +.highlight { + background-color: $code_bg_color; + padding: 10px 12px; + border: $gray_border; + border-radius: 4px; + overflow-x: auto; +} + +.declaration .highlight { + overflow-x: initial; // This allows the scrollbar to show up inside declarations + padding: 0 40px 40px 0; + margin-bottom: -25px; + background-color: transparent; + border: none; +} + +.section-name { + margin: 0; + margin-left: 18px; +} + +.task-group-section { + padding-left: 6px; + border-top: $gray_border; +} + +.task-group { + padding-top: 0px; +} + +.task-name-container { + a[name] { + &:before { + content: ""; + display: block; + padding-top: $content_top_offset; + margin: -$content_top_offset 0 0; + } + } +} + +.item { + padding-top: 8px; + width: 100%; + list-style-type: none; + a[name] { + &:before { + content: ""; + display: block; + padding-top: $content_top_offset; + margin: -$content_top_offset 0 0; + } + } + code { + background-color: transparent; + padding: 0; + } + .token { + padding-left: 3px; + margin-left: 15px; + font-size: 11.9px; + } + .declaration-note { + font-size: .85em; + color: rgba(128,128,128,1); + font-style: italic; + } +} + +.pointer-container { + border-bottom: $gray_border; + left: -23px; + padding-bottom: 13px; + position: relative; + width: 110%; +} + +.pointer { + background: $declaration_bg_color; + border-left: $gray_border; + border-top: $gray_border; + height: 12px; + left: 21px; + top: -7px; + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); + position: absolute; + width: 12px; +} + +.height-container { + display: none; + left: -25px; + padding: 0 25px; + position: relative; + width: 100%; + overflow: hidden; + .section { + background: $declaration_bg_color; + border-bottom: $gray_border; + left: -25px; + position: relative; + width: 100%; + padding-top: 10px; + padding-bottom: 5px; + } +} + +.aside, .language { + padding: 6px 12px; + margin: 12px 0; + border-left: $aside_border; + overflow-y: hidden; + .aside-title { + font-size: 9px; + letter-spacing: 2px; + text-transform: uppercase; + padding-bottom: 0; + margin: 0; + color: $aside_color; + -webkit-user-select: none; + } + p:last-child { + margin-bottom: 0; + } +} + +.language { + border-left: $declaration_language_border; + .aside-title { + color: $declaration_title_language_color; + } +} + +.aside-warning { + border-left: $aside_warning_border; + .aside-title { + color: $aside_warning_color; + } +} + +.graybox { + border-collapse: collapse; + width: 100%; + p { + margin: 0; + word-break: break-word; + min-width: 50px; + } + td { + border: $gray_border; + padding: 5px 25px 5px 10px; + vertical-align: middle; + } + tr td:first-of-type { + text-align: right; + padding: 7px; + vertical-align: top; + word-break: normal; + width: 40px; + } +} + +.slightly-smaller { + font-size: 0.9em; +} + +#footer { + position: absolute; + bottom: 10px; + margin-left: 25px; + p { + margin: 0; + color: #aaa; + font-size: 0.8em; + } +} + +//////////////////////////////// +// Dash +//////////////////////////////// + +html.dash { + header, #breadcrumbs, .sidebar { + display: none; + } + .main-content { + width: $content_wrapper_width; + margin-left: 0; + border: none; + width: 100%; + top: 0; + padding-bottom: 0; + } + .height-container { + display: block; + } + .item .token { + margin-left: 0; + } + .content-wrapper { + width: auto; + } + #footer { + position: static; + } +} diff --git a/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/img/carat.png b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/img/carat.png new file mode 100755 index 0000000000..29d2f7fd49 Binary files /dev/null and b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/img/carat.png differ diff --git a/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/img/dash.png b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/img/dash.png new file mode 100755 index 0000000000..6f694c7a01 Binary files /dev/null and b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/img/dash.png differ diff --git a/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/img/gh.png b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/img/gh.png new file mode 100755 index 0000000000..628da97c70 Binary files /dev/null and b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/img/gh.png differ diff --git a/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/js/jazzy.js b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/js/jazzy.js new file mode 100755 index 0000000000..4ff9455b6b --- /dev/null +++ b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/js/jazzy.js @@ -0,0 +1,40 @@ +window.jazzy = {'docset': false} +if (typeof window.dash != 'undefined') { + document.documentElement.className += ' dash' + window.jazzy.docset = true +} +if (navigator.userAgent.match(/xcode/i)) { + document.documentElement.className += ' xcode' + window.jazzy.docset = true +} + +// On doc load, toggle the URL hash discussion if present +$(document).ready(function() { + if (!window.jazzy.docset) { + var linkToHash = $('a[href="' + window.location.hash +'"]'); + linkToHash.trigger("click"); + } +}); + +// On token click, toggle its discussion and animate token.marginLeft +$(".token").click(function(event) { + if (window.jazzy.docset) { + return; + } + var link = $(this); + var animationDuration = 300; + var tokenOffset = "15px"; + var original = link.css('marginLeft') == tokenOffset; + link.animate({'margin-left':original ? "0px" : tokenOffset}, animationDuration); + $content = link.parent().parent().next(); + $content.slideToggle(animationDuration); + + // Keeps the document from jumping to the hash. + var href = $(this).attr('href'); + if (history.pushState) { + history.pushState({}, '', href); + } else { + location.hash = href; + } + event.preventDefault(); +}); diff --git a/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/js/jquery.min.js b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/js/jquery.min.js new file mode 100755 index 0000000000..ab28a24729 --- /dev/null +++ b/submodules/HockeySDK-iOS/Documentation/Themes/apple/assets/js/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.11.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.1",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h; +if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/\s*$/g,rb={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:k.htmlSerialize?[0,"",""]:[1,"X
","
"]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?""!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("