Add 'submodules/HockeySDK-iOS/' from commit 'c7d0c7026303253e2ac576c02655691e5d314fe2'

git-subtree-dir: submodules/HockeySDK-iOS
git-subtree-mainline: 085acd26c4
git-subtree-split: c7d0c70263
This commit is contained in:
Peter
2019-06-11 18:53:14 +01:00
444 changed files with 57240 additions and 0 deletions

View File

@@ -0,0 +1 @@
*.pbxproj merge=union

25
submodules/HockeySDK-iOS/.gitignore vendored Normal file
View File

@@ -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/*

View File

@@ -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',
],
)

View File

@@ -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 <UIKit/UIKit.h>
@interface BITActivityIndicatorButton : UIButton
- (void)setShowsActivityIndicator:(BOOL)showsIndicator;
@end

View File

@@ -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 */

View File

@@ -0,0 +1,9 @@
#import <UIKit/UIKit.h>
@interface BITAlertAction : UIAlertAction
+ (UIAlertAction * _Nonnull)actionWithTitle:(nullable NSString *)title style:(UIAlertActionStyle)style handler:(void (^_Nullable)(UIAlertAction *_Nonnull))handler;
- (void)invokeAction;
@end

View File

@@ -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

View File

@@ -0,0 +1,40 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
* 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 <UIKit/UIKit.h>
@interface BITAppStoreHeader : UIView
@property (nonatomic, copy) NSString *headerText;
@property (nonatomic, copy) NSString *subHeaderText;
@property (nonatomic, strong) UIImage *iconImage;
@end

View File

@@ -0,0 +1,150 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
* 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 */

View File

@@ -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 <Foundation/Foundation.h>
@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

View File

@@ -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

View File

@@ -0,0 +1,12 @@
#import "BITTelemetryObject.h"
@interface BITApplication : BITTelemetryObject <NSCoding>
@property (nonatomic, copy) NSString *version;
@property (nonatomic, copy) NSString *build;
@property (nonatomic, copy) NSString *typeId;
- (instancetype)initWithCoder:(NSCoder *)coder;
- (void)encodeWithCoder:(NSCoder *)coder;
@end

View File

@@ -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

View File

@@ -0,0 +1,33 @@
/*
* Author: Moritz Haarmann <post@moritzhaarmann.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "BITImageAnnotation.h"
@interface BITArrowImageAnnotation : BITImageAnnotation
@end

View File

@@ -0,0 +1,215 @@
/*
* Author: Moritz Haarmann <post@moritzhaarmann.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "HockeySDK.h"
#if HOCKEYSDK_FEATURE_FEEDBACK
#import "BITArrowImageAnnotation.h"
#import <tgmath.h>
#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 */

View File

@@ -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 <UIKit/UIKit.h>
#import <CoreText/CoreText.h>
//! 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 <NSObject>
@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 <BITAttributedLabel, UIGestureRecognizerDelegate>
/**
* 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 <BITAttributedLabelDelegate> 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 <NSObject>
///-----------------------------------
/// @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 <NSCoding>
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

File diff suppressed because it is too large Load Diff

View File

@@ -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 <UIKit/UIKit.h>
@protocol BITAuthenticationViewControllerDelegate;
@class BITAuthenticator;
@class BITHockeyAppClient;
/**
* View controller handling user interaction for `BITAuthenticator`
*/
@interface BITAuthenticationViewController : UITableViewController
- (instancetype) initWithDelegate:(id<BITAuthenticationViewControllerDelegate>) 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<BITAuthenticationViewControllerDelegate> delegate;
/**
* allows to pre-fill the email-addy
*/
@property (nonatomic, copy) NSString* email;
@end
/**
* BITAuthenticationViewController protocol
*/
@protocol BITAuthenticationViewControllerDelegate<NSObject>
- (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

View File

@@ -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 <tgmath.h>
@interface BITAuthenticationViewController ()<UITextFieldDelegate>
@property (nonatomic, weak) UITextField *emailField;
@property (nonatomic, copy) NSString *password;
@end
@implementation BITAuthenticationViewController
- (instancetype) initWithDelegate:(id<BITAuthenticationViewControllerDelegate>)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 */

View File

@@ -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 <Foundation/Foundation.h>
#import "BITHockeyBaseManager.h"
/**
* Identification Types
*/
typedef NS_ENUM(NSUInteger, BITAuthenticatorIdentificationType) {
/**
* Assigns this app an anonymous user id.
* <br/><br/>
* 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
* <br/><br/>
* This will present a user interface requesting the user to provide their
* HockeyApp user email address.
* <br/><br/>
* 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
* <br/><br/>
* This will present a user interface requesting the user to provide their
* HockeyApp user credentials.
* <br/><br/>
* 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
* <br/><br/>
* 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.
* <br/><br/>
* 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.
* <br/><br/>
* 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.
* <br/><br/>
* 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<APP_ID>`.
*
* @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<APP_ID>` 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 <NSObject>
@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

View File

@@ -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 <sys/stat.h>
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 */

View File

@@ -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 ()<BITAuthenticationViewControllerDelegate>
/**
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<BITAuthenticatorDelegate> 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 */

View File

@@ -0,0 +1,13 @@
#import "BITTelemetryObject.h"
#import "BITTelemetryData.h"
@interface BITBase : BITTelemetryData <NSCoding>
@property (nonatomic, copy) NSString *baseType;
- (instancetype)initWithCoder:(NSCoder *)coder;
- (void)encodeWithCoder:(NSCoder *)coder;
@end

View File

@@ -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

View File

@@ -0,0 +1,33 @@
/*
* Author: Moritz Haarmann <post@moritzhaarmann.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "BITImageAnnotation.h"
@interface BITBlurImageAnnotation : BITImageAnnotation
@end

View File

@@ -0,0 +1,109 @@
/*
* Author: Moritz Haarmann <post@moritzhaarmann.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "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 */

View File

@@ -0,0 +1,17 @@
#import <Foundation/Foundation.h>
#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 */

View File

@@ -0,0 +1,144 @@
#import "BITCategoryContainer.h"
#import "HockeySDKFeatureConfig.h"
#import <objc/runtime.h>
#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 <zlib.h>
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 */

View File

@@ -0,0 +1,47 @@
#import <Foundation/Foundation.h>
#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 */

View File

@@ -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 <stdatomic.h>
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 */

View File

@@ -0,0 +1,125 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#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 */

View File

@@ -0,0 +1,59 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* 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

View File

@@ -0,0 +1,48 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* 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

View File

@@ -0,0 +1,49 @@
/*
* Author: Gwynne Raskind <gwraskin@microsoft.com>
*
* 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 <Foundation/Foundation.h>
#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

View File

@@ -0,0 +1,247 @@
/*
* Author: Gwynne Raskind <gwraskin@microsoft.com>
*
* 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 <vector>
#import <cxxabi.h>
#import <exception>
#import <stdexcept>
#import <typeinfo>
#import <string>
#import <pthread.h>
#import <dlfcn.h>
#import <execinfo.h>
#import <libkern/OSAtomic.h>
typedef std::vector<BITCrashUncaughtCXXExceptionHandler> 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<cxa_throw_func>(dlsym(RTLD_NEXT, "__cxa_throw"));
__real_objc_ehtype_vtable = reinterpret_cast<const void **>(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<void **>(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<BITCrashCXXExceptionTSInfo *>(pthread_getspecific(_BITCrashCXXExceptionInfoTSDKey));
if (!info) {
info = reinterpret_cast<BITCrashCXXExceptionTSInfo *>(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<void **>(&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<const void *>(&p);
info.exception_type_name = __cxxabiv1::__cxa_current_exception_type()->name();
BITCrashCXXExceptionTSInfo *recorded_info = reinterpret_cast<BITCrashCXXExceptionTSInfo *>(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<uintptr_t *>(&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 */

View File

@@ -0,0 +1,123 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
/**
* 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

View File

@@ -0,0 +1,81 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "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 */

View File

@@ -0,0 +1,48 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
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

View File

@@ -0,0 +1,441 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
* 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 <Foundation/Foundation.h>
#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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,155 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
@class BITCrashManager;
@class BITHockeyAttachment;
/**
The `BITCrashManagerDelegate` formal protocol defines methods further configuring
the behaviour of `BITCrashManager`.
*/
@protocol BITCrashManagerDelegate <NSObject>
@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

View File

@@ -0,0 +1,113 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* 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 <CrashReporter/CrashReporter.h>
@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 */

View File

@@ -0,0 +1,57 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
/**
* 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

View File

@@ -0,0 +1,39 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "HockeySDK.h"
#if HOCKEYSDK_FEATURE_CRASH_REPORTER
#import "BITCrashMetaData.h"
@implementation BITCrashMetaData
@end
#endif

View File

@@ -0,0 +1,75 @@
/*
* Authors:
* Landon Fuller <landonf@plausiblelabs.com>
* Damian Morris <damian@moso.com.au>
* Andreas Linde <mail@andreaslinde.de>
*
* 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 <Foundation/Foundation.h>
@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

View File

@@ -0,0 +1,928 @@
/*
* Authors:
* Landon Fuller <landonf@plausiblelabs.com>
* Damian Morris <damian@moso.com.au>
* Andreas Linde <mail@andreaslinde.de>
*
* 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 <CrashReporter/CrashReporter.h>
#import <mach-o/dyld.h>
#import <mach-o/getsect.h>
#import <mach-o/ldsyms.h>
#import <dlfcn.h>
#import <Availability.h>
#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 `<ecx/rsi/r1/x1 ...> - <image base>`, 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 <uuid> 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 */

View File

@@ -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 */

View File

@@ -0,0 +1,13 @@
#import "BITBase.h"
@class BITTelemetryData;
@interface BITData : BITBase <NSCoding>
@property (nonatomic, strong) BITTelemetryData *baseData;
- (instancetype)initWithCoder:(NSCoder *)coder;
- (void)encodeWithCoder:(NSCoder *)coder;
@end

View File

@@ -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

View File

@@ -0,0 +1,27 @@
#import "BITTelemetryObject.h"
@interface BITDevice : BITTelemetryObject <NSCoding>
@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

View File

@@ -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

View File

@@ -0,0 +1,5 @@
#import "BITTelemetryData.h"
@interface BITDomain : BITTelemetryData <NSCoding>
@end

View File

@@ -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

View File

@@ -0,0 +1,23 @@
#import "BITTelemetryObject.h"
@class BITBase;
@interface BITEnvelope : BITTelemetryObject <NSCoding>
@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

View File

@@ -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

View File

@@ -0,0 +1,9 @@
#import "BITDomain.h"
@interface BITEventData : BITDomain <NSCoding>
@property (nonatomic, copy, readonly) NSString *envelopeTypeName;
@property (nonatomic, copy, readonly) NSString *dataTypeName;
@property (nonatomic, strong) NSDictionary *measurements;
@end

View File

@@ -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

View File

@@ -0,0 +1,75 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <UIKit/UIKit.h>
#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 <BITFeedbackComposeViewControllerDelegate>
///-----------------------------------------------------------------------------
/// @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

View File

@@ -0,0 +1,150 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "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 */

View File

@@ -0,0 +1,97 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <UIKit/UIKit.h>
#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 <UITextViewDelegate>
///-----------------------------------------------------------------------------
/// @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<BITFeedbackComposeViewControllerDelegate> 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

View File

@@ -0,0 +1,690 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "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 () <BITFeedbackUserDataDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIActionSheetDelegate, BITImageAnnotationDelegate> {
}
@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 <numberOfViewsToCreate; i++) {
UIButton *newImageButton = [UIButton buttonWithType:UIButtonTypeCustom];
[newImageButton addTarget:self action:@selector(imageButtonAction:) forControlEvents:UIControlEventTouchUpInside];
[self.attachmentScrollViewImageViews addObject:newImageButton];
[self.attachmentScrollView addSubview:newImageButton];
}
}
int index = 0;
CGFloat currentYOffset = 0.0;
NSEnumerator *reverseAttachments = self.imageAttachments.reverseObjectEnumerator;
for (BITFeedbackMessageAttachment *attachment in reverseAttachments.allObjects){
UIButton *imageButton = self.attachmentScrollViewImageViews[index];
UIImage *image = [attachment thumbnailWithSize:CGSizeMake(100, 100)];
// determine the factor by which we scale..
CGFloat scaleFactor = CGRectGetWidth(self.attachmentScrollView.frame) / image.size.width;
CGFloat height = image.size.height * scaleFactor;
imageButton.frame = CGRectInset(CGRectMake(0, currentYOffset, scaleFactor * image.size.width, height), 10, 10);
currentYOffset += height;
[imageButton setImage:image forState:UIControlStateNormal];
index++;
}
[self.attachmentScrollView setContentSize:CGSizeMake(CGRectGetWidth(self.attachmentScrollView.frame), currentYOffset)];
[self updateBarButtonState];
}
- (void)updateBarButtonState {
if (self.textView.text.length > 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 */

View File

@@ -0,0 +1,76 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
#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 <NSObject>
@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

View File

@@ -0,0 +1,71 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <UIKit/UIKit.h>
#import "BITFeedbackMessage.h"
#import "BITAttributedLabel.h"
@class BITFeedbackMessageAttachment;
@protocol BITFeedbackListViewCellDelegate <NSObject>
- (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<BITFeedbackListViewCellDelegate> delegate;
+ (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width;
- (void)setAttachments:(NSArray *)attachments;
@end

View File

@@ -0,0 +1,362 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "HockeySDK.h"
#if HOCKEYSDK_FEATURE_FEEDBACK
#import "HockeySDKPrivate.h"
#import "BITFeedbackListViewCell.h"
#import "BITFeedbackMessageAttachment.h"
#import "BITActivityIndicatorButton.h"
#import "BITFeedbackManagerPrivate.h"
#import <tgmath.h>
#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 */

View File

@@ -0,0 +1,61 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <UIKit/UIKit.h>
#import <QuickLook/QuickLook.h>
#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 <UITableViewDelegate, UITableViewDataSource, UIActionSheetDelegate, QLPreviewControllerDataSource> {
}
@end

View File

@@ -0,0 +1,804 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "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 <QuartzCore/QuartzCore.h>
#import <QuickLook/QuickLook.h>
#define DEFAULT_TEXTCOLOR BIT_RGBCOLOR(75, 75, 75)
#define BORDER_COLOR BIT_RGBCOLOR(215, 215, 215)
@interface BITFeedbackListViewController () <BITFeedbackUserDataDelegate, BITFeedbackComposeViewControllerDelegate, BITAttributedLabelDelegate, BITFeedbackListViewCellDelegate>
@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 <QLPreviewItem>)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 */

View File

@@ -0,0 +1,365 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
#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

File diff suppressed because it is too large Load Diff

View File

@@ -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 <Foundation/Foundation.h>
#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 <NSObject, BITFeedbackComposeViewControllerDelegate>
@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

View File

@@ -0,0 +1,133 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
* 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<BITFeedbackManagerDelegate> 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 */

View File

@@ -0,0 +1,102 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
@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

View File

@@ -0,0 +1,120 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "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 */

View File

@@ -0,0 +1,68 @@
/*
* Author: Moritz Haarmann <post@moritzhaarmann.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <QuickLook/QuickLook.h>
/**
* An individual feedback message attachment
*/
@interface BITFeedbackMessageAttachment : NSObject<NSCoding, QLPreviewItem>
@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

View File

@@ -0,0 +1,269 @@
/*
* Author: Moritz Haarmann <post@moritzhaarmann.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "HockeySDK.h"
#if HOCKEYSDK_FEATURE_FEEDBACK
#import "BITFeedbackMessageAttachment.h"
#import "BITHockeyHelper.h"
#import "HockeySDKPrivate.h"
#import <MobileCoreServices/MobileCoreServices.h>
#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<NSCopying> 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 */

View File

@@ -0,0 +1,51 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <UIKit/UIKit.h>
@protocol BITFeedbackUserDataDelegate;
@interface BITFeedbackUserDataViewController : UITableViewController <UITextFieldDelegate>
@property (nonatomic, weak) id <BITFeedbackUserDataDelegate> delegate;
@end
///////////////////////////////////////////////////////////////////////////////////////////////////
@protocol BITFeedbackUserDataDelegate <NSObject>
@required
// cancel action is invoked
- (void)userDataUpdateCancelled;
// save action is invoked and all required data available
- (void)userDataUpdateFinished;
@end

View File

@@ -0,0 +1,266 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "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 */

View File

@@ -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 <Foundation/Foundation.h>
#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

View File

@@ -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 <Foundation/Foundation.h>
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

View File

@@ -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

View File

@@ -0,0 +1,68 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* 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 <Foundation/Foundation.h>
/**
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<NSCoding>
/**
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

View File

@@ -0,0 +1,81 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* 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 */

View File

@@ -0,0 +1,86 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
/**
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

View File

@@ -0,0 +1,372 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "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 <sys/sysctl.h>
#import <mach-o/dyld.h>
#import <mach-o/loader.h>
// 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

View File

@@ -0,0 +1,94 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#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

View File

@@ -0,0 +1,38 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <UIKit/UIKit.h>
@interface BITHockeyBaseViewController : UITableViewController
@property (nonatomic, readwrite) BOOL modalAnimated;
- (instancetype)initWithModalStyle:(BOOL)modal;
- (instancetype)initWithStyle:(UITableViewStyle)style modal:(BOOL)modal;
@end

View File

@@ -0,0 +1,109 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "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 */

View File

@@ -0,0 +1,48 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#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

View File

@@ -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

View File

@@ -0,0 +1,109 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
// Adapted from 0xceds post at http://stackoverflow.com/questions/34732814/how-should-i-handle-logs-in-an-objective-c-library/34732815#34732815
#import <Foundation/Foundation.h>
#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

View File

@@ -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

View File

@@ -0,0 +1,3 @@
#import "BITHockeyLogger.h"
FOUNDATION_EXPORT BITLogHandler const defaultLogHandler;

View File

@@ -0,0 +1,618 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
* 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 <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#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:@"<AppIdentifierFromHockeyApp>"
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:@"<AppIdentifierFromHockeyApp>"];
@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:@"<AppIdentifierFromHockeyApp>"
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<BITHockeyManagerDelegate>)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:@"<AppIdentifierForBetaAppFromHockeyApp>"
liveIdentifier:@"<AppIdentifierForLiveAppFromHockeyApp>"
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<BITHockeyManagerDelegate>)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<BITHockeyManagerDelegate> 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

View File

@@ -0,0 +1,756 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
* 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 <stdint.h>
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<BITHockeyManagerDelegate>)delegate {
self.delegate = delegate;
self.appIdentifier = [appIdentifier copy];
[self initializeModules];
}
- (void)configureWithBetaIdentifier:(NSString *)betaIdentifier liveIdentifier:(NSString *)liveIdentifier delegate:(id<BITHockeyManagerDelegate>)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<BITHockeyManagerDelegate>)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<BITHockeyManagerDelegate> 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<BITHockeyManagerDelegate> 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<BITHockeyManagerDelegate> 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

View File

@@ -0,0 +1,230 @@
/*
* Author: Andreas Linde <mail@andreaslinde.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
#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 <NSObject
#if HOCKEYSDK_FEATURE_CRASH_REPORTER
, BITCrashManagerDelegate
#endif
#if HOCKEYSDK_FEATURE_UPDATES
, BITUpdateManagerDelegate
#endif
#if HOCKEYSDK_FEATURE_FEEDBACK
, BITFeedbackManagerDelegate
#endif
#if HOCKEYSDK_FEATURE_AUTHENTICATOR
, BITAuthenticatorDelegate
#endif
>
@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

View File

@@ -0,0 +1,40 @@
/*
* Author: Moritz Haarmann <post@moritzhaarmann.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <UIKit/UIKit.h>
@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

View File

@@ -0,0 +1,43 @@
/*
* Author: Moritz Haarmann <post@moritzhaarmann.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "HockeySDK.h"
#if HOCKEYSDK_FEATURE_FEEDBACK
#import "BITImageAnnotation.h"
@implementation BITImageAnnotation
-(BOOL)resizable {
return NO;
}
@end
#endif

View File

@@ -0,0 +1,45 @@
/*
* Author: Moritz Haarmann <post@moritzhaarmann.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import <UIKit/UIKit.h>
@class BITImageAnnotationViewController;
@protocol BITImageAnnotationDelegate <NSObject>
- (void)annotationControllerDidCancel:(BITImageAnnotationViewController *)annotationController;
- (void)annotationController:(BITImageAnnotationViewController *)annotationController didFinishWithImage:(UIImage *)image;
@end
@interface BITImageAnnotationViewController : UIViewController
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, weak) id<BITImageAnnotationDelegate> delegate;
@end

View File

@@ -0,0 +1,415 @@
/*
* Author: Moritz Haarmann <post@moritzhaarmann.de>
*
* Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#import "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 */

Some files were not shown because too many files have changed in this diff Show More