├── .gitignore ├── FluxtreamCapture.xcodeproj └── project.pbxproj ├── FluxtreamCapture ├── 19-gear.png ├── 19-gear@2x.png ├── 20-no.png ├── 20-no@2x.png ├── 29-heart.png ├── 29-heart@2x.png ├── 86-camera.png ├── 86-camera@2x.png ├── BTAppDelegate.h ├── BTAppDelegate.mm ├── BTCommentBadge.h ├── BTCommentBadge.m ├── BTFirstViewController.h ├── BTFirstViewController.mm ├── BTPhotoAsset.h ├── BTPhotoAsset.m ├── BTPhotoCell.xib ├── BTPhotoDetailCommentViewController.h ├── BTPhotoDetailCommentViewController.m ├── BTPhotoDetailViewController.h ├── BTPhotoDetailViewController.mm ├── BTPhotoImageUploadRequest.h ├── BTPhotoImageUploadRequest.mm ├── BTPhotoMetadataRequest.h ├── BTPhotoMetadataRequest.mm ├── BTPhotoMetadataUploadRequest.h ├── BTPhotoMetadataUploadRequest.mm ├── BTPhotoTagsForUserRequest.h ├── BTPhotoTagsForUserRequest.mm ├── BTPhotoUploader.h ├── BTPhotoUploader.mm ├── BTPhotosViewController.h ├── BTPhotosViewController.m ├── BTSettingsViewController.h ├── BTSettingsViewController.mm ├── Constants.h ├── Default-568h@2x.png ├── Default.png ├── Default@2x.png ├── FluxtreamCapture-Info.plist ├── FluxtreamCapture-Prefix.pch ├── TestFlightSDK3.0.2 │ ├── README.md │ ├── TestFlight+AsyncLogging.h │ ├── TestFlight+ManualSessions.h │ ├── TestFlight.h │ ├── libTestFlight.a │ └── release_notes.md ├── en.lproj │ ├── InfoPlist.strings │ └── MainStoryboard_iPhone.storyboard ├── locked.png ├── main.mm ├── second.png ├── second@2x.png └── unlocked.png ├── FluxtreamUploaderCpp.h ├── FluxtreamUploaderCpp.mm ├── README.md ├── Shared ├── BTPhoneTracker.h ├── BTPhoneTracker.mm ├── BTPulseTracker.h ├── BTPulseTracker.mm ├── ChannelList.cpp ├── ChannelList.h ├── FluxtreamUploader.h ├── FluxtreamUploader.mm ├── FluxtreamUploaderObjc.h ├── FluxtreamUploaderObjc.mm ├── Logger.h ├── Logger.mm ├── Mutex.h ├── NSUtils.h ├── NSUtils.mm ├── Nickname.cpp ├── Nickname.h ├── RawZeoReader.cpp ├── RawZeoReader.h ├── RawZeoRelay.cpp ├── RawZeoRelay.h ├── Samples.cpp ├── Samples.h ├── TextViewLogger.h ├── TextViewLogger.mm ├── UUID.cpp ├── UUID.h ├── Utils.cpp ├── Utils.h ├── heartbeat_s1.aiff └── heartbeat_s2.aiff ├── ZeoRelay.xcodeproj └── project.pbxproj ├── ZeoRelay ├── AppDelegate.h ├── AppDelegate.m ├── ZeoRelay-Info.plist ├── ZeoRelay-Prefix.pch ├── ZeoRelay.xcdatamodeld │ ├── .xccurrentversion │ └── ZeoRelay.xcdatamodel │ │ └── contents ├── ZeoRelayTests │ ├── ZeoRelayTests-Info.plist │ ├── ZeoRelayTests.h │ ├── ZeoRelayTests.m │ └── en.lproj │ │ └── InfoPlist.strings ├── en.lproj │ ├── Credits.rtf │ ├── InfoPlist.strings │ └── MainMenu.xib └── main.m ├── appicon.png └── appicon@2x.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.xcworkspace 2 | xcuserdata 3 | *~ 4 | .DS_Store 5 | *.ipa 6 | -------------------------------------------------------------------------------- /FluxtreamCapture/19-gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/19-gear.png -------------------------------------------------------------------------------- /FluxtreamCapture/19-gear@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/19-gear@2x.png -------------------------------------------------------------------------------- /FluxtreamCapture/20-no.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/20-no.png -------------------------------------------------------------------------------- /FluxtreamCapture/20-no@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/20-no@2x.png -------------------------------------------------------------------------------- /FluxtreamCapture/29-heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/29-heart.png -------------------------------------------------------------------------------- /FluxtreamCapture/29-heart@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/29-heart@2x.png -------------------------------------------------------------------------------- /FluxtreamCapture/86-camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/86-camera.png -------------------------------------------------------------------------------- /FluxtreamCapture/86-camera@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/86-camera@2x.png -------------------------------------------------------------------------------- /FluxtreamCapture/BTAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTAppDelegate.h 3 | // Stetho 4 | // 5 | // Created by Nick Winter on 10/20/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "Logger.h" 12 | #import "BTFirstViewController.h" 13 | #import "BTPhoneTracker.h" 14 | #import "BTPulseTracker.h" 15 | #import "BTPhotoUploader.h" 16 | 17 | @interface BTAppDelegate : UIResponder 18 | 19 | @property (strong, nonatomic) UIWindow *window; 20 | @property (weak) BTFirstViewController *firstViewController; 21 | @property (strong) BTPulseTracker *pulseTracker; 22 | @property (strong) BTPhoneTracker *phoneTracker; 23 | @property (strong) BTPhotoUploader *photoUploader; 24 | @property (readonly) Logger *logger; 25 | 26 | - (void)selectSettingsTab; 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTAppDelegate.mm: -------------------------------------------------------------------------------- 1 | // 2 | // BTAppDelegate.mm 3 | // Stetho 4 | // 5 | // Created by Nick Winter on 10/20/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import "BTAppDelegate.h" 10 | #import "TestFlight.h" 11 | #import "Constants.h" 12 | 13 | @implementation BTAppDelegate 14 | 15 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 16 | { 17 | #pragma clang diagnostic push 18 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 19 | //[TestFlight setDeviceIdentifier:[[UIDevice currentDevice] uniqueIdentifier]]; 20 | #pragma clang diagnostic pop 21 | [TestFlight takeOff:@"f9c4aa0a-550e-4c8c-bdce-1b9dfa6b6f1b"]; 22 | 23 | _pulseTracker = [[BTPulseTracker alloc] init]; 24 | _phoneTracker = [[BTPhoneTracker alloc] init]; 25 | _photoUploader = [BTPhotoUploader sharedPhotoUploader]; 26 | 27 | [self registerDefaults]; 28 | 29 | return YES; 30 | } 31 | 32 | 33 | - (void)registerDefaults 34 | { 35 | NSMutableDictionary *defaultValues = [NSMutableDictionary dictionary]; 36 | 37 | [defaultValues setObject:@"" forKey:DEFAULTS_USERNAME]; 38 | [defaultValues setObject:@"" forKey:DEFAULTS_PASSWORD]; 39 | [defaultValues setObject:@"fluxtream.org" forKey:DEFAULTS_SERVER]; 40 | 41 | [defaultValues setObject:[NSNumber numberWithBool:YES] forKey:DEFAULTS_FIRSTRUN]; 42 | 43 | [defaultValues setObject:[NSNumber numberWithBool:YES] forKey:DEFAULTS_RECORD_LOCATION]; 44 | [defaultValues setObject:[NSNumber numberWithBool:NO] forKey:DEFAULTS_RECORD_MOTION]; 45 | [defaultValues setObject:[NSNumber numberWithBool:YES] forKey:DEFAULTS_RECORD_APP_STATS]; 46 | [defaultValues setObject:[NSNumber numberWithBool:YES] forKey:DEFAULTS_RECORD_HEARTRATE]; 47 | [defaultValues setObject:[NSNumber numberWithBool:NO] forKey:DEFAULTS_HEARTBEAT_SOUND]; 48 | 49 | [defaultValues setObject:[NSNumber numberWithBool:NO] forKey:DEFAULTS_PHOTO_ORIENTATION_PORTRAIT]; 50 | [defaultValues setObject:[NSNumber numberWithBool:NO] forKey:DEFAULTS_PHOTO_ORIENTATION_UPSIDE_DOWN]; 51 | [defaultValues setObject:[NSNumber numberWithBool:NO] forKey:DEFAULTS_PHOTO_ORIENTATION_LANDSCAPE_LEFT]; 52 | [defaultValues setObject:[NSNumber numberWithBool:NO] forKey:DEFAULTS_PHOTO_ORIENTATION_LANDSCAPE_RIGHT]; 53 | 54 | [defaultValues setObject:[NSDate date] forKey:DEFAULTS_PHOTO_ORIENTATION_SETTINGS_CHANGED]; 55 | 56 | [[NSUserDefaults standardUserDefaults] registerDefaults:defaultValues]; 57 | } 58 | 59 | - (void)selectSettingsTab 60 | { 61 | UITabBarController *tabBarController = (UITabBarController *) self.window.rootViewController; 62 | [tabBarController setSelectedIndex:2]; 63 | } 64 | 65 | 66 | 67 | - (void)applicationWillResignActive:(UIApplication *)application 68 | { 69 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 70 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 71 | [self.logger logVerbose:@"Becoming inactive."]; 72 | } 73 | 74 | - (void)applicationDidEnterBackground:(UIApplication *)application 75 | { 76 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 77 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 78 | [self.logger logVerbose:@"Entered background."]; 79 | 80 | [self savePhotosArray]; 81 | } 82 | 83 | - (void)applicationWillEnterForeground:(UIApplication *)application 84 | { 85 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 86 | [self.logger logVerbose:@"Entering foreground."]; 87 | [[NSNotificationCenter defaultCenter] postNotificationName:BT_NOTIFICATION_APP_FOREGROUNDED object:self]; 88 | } 89 | 90 | - (void)applicationDidBecomeActive:(UIApplication *)application 91 | { 92 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 93 | [self.logger logVerbose:@"Became active."]; 94 | } 95 | 96 | - (void)applicationWillTerminate:(UIApplication *)application 97 | { 98 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 99 | [self savePhotosArray]; 100 | [[NSUserDefaults standardUserDefaults] synchronize]; 101 | } 102 | 103 | - (void)savePhotosArray 104 | { 105 | BOOL success = [_photoUploader savePhotosArray]; 106 | if (success) { 107 | NSLog(@"Saved photos array"); 108 | } else { 109 | NSLog(@"!! Photos array was not saved"); 110 | } 111 | } 112 | 113 | @end 114 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTCommentBadge.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTCommentBadge.h 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 3/26/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | // A custom UIView to draw the speech bubble badge that appears on photos in 10 | // the collection view that have comments or tags 11 | 12 | #import 13 | 14 | @interface BTCommentBadge : UIView 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTCommentBadge.m: -------------------------------------------------------------------------------- 1 | // 2 | // BTCommentBadge.m 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 3/26/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import "BTCommentBadge.h" 10 | 11 | @implementation BTCommentBadge 12 | 13 | - (id)initWithFrame:(CGRect)frame 14 | { 15 | self = [super initWithFrame:frame]; 16 | if (self) { 17 | // Initialization code 18 | } 19 | return self; 20 | } 21 | 22 | 23 | - (void)drawRect:(CGRect)rect 24 | { 25 | //// General Declarations 26 | CGContextRef context = UIGraphicsGetCurrentContext(); 27 | 28 | //// Color Declarations 29 | UIColor* fillColor = [UIColor colorWithRed: 1 green: 1 blue: 1 alpha: 1]; 30 | UIColor* strokeColor = [UIColor colorWithRed: 0.667f green: 0.667f blue: 0.667f alpha: 1]; 31 | UIColor* strokeColor2 = [UIColor colorWithRed: 0 green: 0 blue: 0 alpha: 1]; 32 | 33 | //// Shadow Declarations 34 | UIColor* shadow = strokeColor2; 35 | CGSize shadowOffset = CGSizeMake(0.1f, -0.1f); 36 | CGFloat shadowBlurRadius = 2.5; 37 | 38 | //// Bezier 2 Drawing 39 | UIBezierPath* bezier2Path = [UIBezierPath bezierPath]; 40 | [bezier2Path moveToPoint: CGPointMake(22.5, 5.29f)]; 41 | [bezier2Path addLineToPoint: CGPointMake(22.5, 10.03f)]; 42 | [bezier2Path addCurveToPoint: CGPointMake(18.5, 13.82f) controlPoint1: CGPointMake(22.5, 12.12f) controlPoint2: CGPointMake(20.71f, 13.82f)]; 43 | [bezier2Path addLineToPoint: CGPointMake(11.93f, 13.82f)]; 44 | [bezier2Path addLineToPoint: CGPointMake(8.5, 19.5)]; 45 | [bezier2Path addLineToPoint: CGPointMake(8.5, 13.82f)]; 46 | [bezier2Path addLineToPoint: CGPointMake(5.5, 13.82f)]; 47 | [bezier2Path addCurveToPoint: CGPointMake(1.5, 10.03f) controlPoint1: CGPointMake(3.29f, 13.82f) controlPoint2: CGPointMake(1.5, 12.12f)]; 48 | [bezier2Path addLineToPoint: CGPointMake(1.5, 5.29f)]; 49 | [bezier2Path addCurveToPoint: CGPointMake(5.5, 1.5) controlPoint1: CGPointMake(1.5, 3.2f) controlPoint2: CGPointMake(3.29f, 1.5)]; 50 | [bezier2Path addLineToPoint: CGPointMake(18.5, 1.5)]; 51 | [bezier2Path addCurveToPoint: CGPointMake(22.5, 5.29f) controlPoint1: CGPointMake(20.71f, 1.5) controlPoint2: CGPointMake(22.5, 3.2f)]; 52 | [bezier2Path closePath]; 53 | CGContextSaveGState(context); 54 | CGContextSetShadowWithColor(context, shadowOffset, shadowBlurRadius, shadow.CGColor); 55 | [strokeColor setFill]; 56 | [bezier2Path fill]; 57 | CGContextRestoreGState(context); 58 | 59 | [strokeColor setStroke]; 60 | bezier2Path.lineWidth = 1; 61 | [bezier2Path stroke]; 62 | 63 | 64 | //// Oval Drawing 65 | UIBezierPath* ovalPath = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(4, 6, 4, 4)]; 66 | [fillColor setFill]; 67 | [ovalPath fill]; 68 | 69 | 70 | //// Oval 2 Drawing 71 | UIBezierPath* oval2Path = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(10, 6, 4, 4)]; 72 | [fillColor setFill]; 73 | [oval2Path fill]; 74 | 75 | 76 | //// Oval 3 Drawing 77 | UIBezierPath* oval3Path = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(16, 6, 4, 4)]; 78 | [fillColor setFill]; 79 | [oval3Path fill]; 80 | } 81 | 82 | 83 | @end 84 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTFirstViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTFirstViewController.h 3 | // Stetho 4 | // 5 | // Created by Nick Winter on 10/20/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TextViewLogger.h" 11 | 12 | @interface BTFirstViewController : UIViewController 13 | { 14 | NSTimer *hrStatusTimer; 15 | NSTimer *uploadStatusTimer; 16 | } 17 | @property (weak, nonatomic) IBOutlet UILabel *heartRateLabel; 18 | @property (weak, nonatomic) IBOutlet UILabel *variabilityLabel; 19 | @property (weak, nonatomic) IBOutlet UIImageView *heartImage; 20 | @property (weak, nonatomic) IBOutlet UILabel *hrConnectionStatusLabel; 21 | @property (weak, nonatomic) IBOutlet UILabel *hrDataStatusLabel; 22 | @property (weak, nonatomic) IBOutlet UILabel *hrUploadStatusLabel; 23 | @property (weak, nonatomic) IBOutlet UITextView *hrLogView; 24 | @property (weak, nonatomic) IBOutlet UILabel *buildLabel; 25 | @property (weak, nonatomic) IBOutlet UIButton *heartLockButton; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTFirstViewController.mm: -------------------------------------------------------------------------------- 1 | // 2 | // BTFirstViewController.m 3 | // Stetho 4 | // 5 | // Created by Nick Winter on 10/20/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import "BTFirstViewController.h" 10 | #import 11 | 12 | #include "Utils.h" 13 | #include "NSUtils.h" 14 | 15 | #include "Constants.h" 16 | 17 | #import "BTAppDelegate.h" 18 | 19 | @interface BTFirstViewController () 20 | @property (assign) SystemSoundID heartbeatS1Sound; 21 | @property (assign) SystemSoundID heartbeatS2Sound; 22 | 23 | - (void)onPulse:(NSNotification *)note; 24 | - (void)onHRDataReceived:(NSNotification *)note; 25 | 26 | @end 27 | 28 | @implementation BTFirstViewController 29 | @synthesize heartRateLabel; 30 | @synthesize variabilityLabel; 31 | @synthesize heartImage; 32 | @synthesize heartbeatS1Sound; 33 | @synthesize heartbeatS2Sound; 34 | 35 | #pragma mark - View lifecycle 36 | 37 | - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 38 | { 39 | self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 40 | return self; 41 | } 42 | 43 | - (void)viewDidLoad 44 | { 45 | [super viewDidLoad]; 46 | 47 | #ifdef DEBUG 48 | const char *buildType = "debug"; 49 | #else 50 | const char *buildType = "release"; 51 | #endif 52 | 53 | 54 | NSString * version = [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleShortVersionString"]; 55 | NSString * build = [[NSBundle mainBundle] objectForInfoDictionaryKey: (NSString *)kCFBundleVersionKey]; 56 | 57 | self.buildLabel.text = [NSString stringWithFormat:@"Version %@ %s build %@ %s", version, buildType, build, __DATE__]; 58 | 59 | // Do any additional setup after loading the view, typically from a nib. 60 | NSString *soundPathS1 = [[NSBundle mainBundle] pathForResource:@"heartbeat_s1" ofType:@"aiff"]; 61 | NSString *soundPathS2 = [[NSBundle mainBundle] pathForResource:@"heartbeat_s2" ofType:@"aiff"]; 62 | AudioServicesCreateSystemSoundID((__bridge CFURLRef)[NSURL fileURLWithPath:soundPathS1], &heartbeatS1Sound); 63 | AudioServicesCreateSystemSoundID((__bridge CFURLRef)[NSURL fileURLWithPath:soundPathS2], &heartbeatS2Sound); 64 | 65 | BTAppDelegate *appDelegate = (BTAppDelegate *)[[UIApplication sharedApplication] delegate]; 66 | appDelegate.firstViewController = self; 67 | 68 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 69 | if ([defaults boolForKey:DEFAULTS_FIRSTRUN] == YES) { 70 | [defaults setBool:NO forKey:DEFAULTS_FIRSTRUN]; 71 | [defaults synchronize]; 72 | 73 | UIAlertView *message = [[UIAlertView alloc] initWithTitle:@"Welcome" 74 | message:@"Please enter your Fluxtream username and password (register at fluxtream.com)" 75 | delegate:self 76 | cancelButtonTitle:@"OK" 77 | otherButtonTitles:nil]; 78 | [message show]; 79 | 80 | [appDelegate selectSettingsTab]; 81 | } 82 | 83 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onPulse:) name:BT_NOTIFICATION_PULSE object:[appDelegate pulseTracker]]; 84 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onHRDataReceived:) name:BT_NOTIFICATION_HR_DATA object:[appDelegate pulseTracker]]; 85 | 86 | // TODO(rsargent) subscribe to auth failed, queued data 87 | // pop up alert with UIAlertView 88 | 89 | TextViewLogger *logger = [[TextViewLogger alloc] init]; 90 | logger.maxDisplayedVerbosity = kLogNormal; 91 | logger.textView = self.hrLogView; 92 | 93 | BTPulseTracker *pulseTracker = [(BTAppDelegate *)[[UIApplication sharedApplication] delegate] pulseTracker]; 94 | pulseTracker.logger = logger; 95 | 96 | hrStatusTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateHRStatus) userInfo:nil repeats:YES]; 97 | uploadStatusTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateUploadStatus) userInfo:nil repeats:YES]; 98 | 99 | self.heartLockButton.selected = pulseTracker.connectOnlyToNickname; 100 | } 101 | 102 | - (BTPulseTracker*)pulseTracker { 103 | return [(BTAppDelegate *)[[UIApplication sharedApplication] delegate] pulseTracker]; 104 | } 105 | 106 | - (void)didReceiveMemoryWarning 107 | { 108 | [super didReceiveMemoryWarning]; 109 | // Dispose of any resources that can be recreated. 110 | } 111 | 112 | - (void)viewDidUnload { 113 | AudioServicesDisposeSystemSoundID(heartbeatS1Sound); 114 | AudioServicesDisposeSystemSoundID(heartbeatS2Sound); 115 | [self setHeartRateLabel:nil]; 116 | [self setVariabilityLabel:nil]; 117 | [super viewDidUnload]; 118 | } 119 | 120 | - (void)dealloc 121 | { 122 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 123 | } 124 | 125 | #pragma mark - Pulse display 126 | 127 | - (void)onPulse:(NSNotification *)note { 128 | BTPulseTracker *pulseTracker = [(BTAppDelegate *)[[UIApplication sharedApplication] delegate] pulseTracker]; 129 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 130 | 131 | float PULSESCALE = 1.5; 132 | double PULSEDURATION = 0.2 * 60.0 / pulseTracker.heartRate; 133 | 134 | [UIView animateWithDuration:PULSEDURATION delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{ 135 | if ([defaults boolForKey:DEFAULTS_HEARTBEAT_SOUND] == YES) AudioServicesPlaySystemSound(heartbeatS1Sound); 136 | self.heartImage.transform = CGAffineTransformMakeScale(PULSESCALE, PULSESCALE); 137 | } completion:^(BOOL finished){ 138 | [UIView animateWithDuration:PULSEDURATION delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ 139 | if ([defaults boolForKey:DEFAULTS_HEARTBEAT_SOUND] == YES) AudioServicesPlaySystemSound(heartbeatS2Sound); 140 | self.heartImage.transform = CGAffineTransformIdentity; 141 | } completion:nil]; 142 | }]; 143 | } 144 | 145 | - (void)onHRDataReceived:(NSNotification *)note { 146 | BTPulseTracker *pulseTracker = [(BTAppDelegate *)[[UIApplication sharedApplication] delegate] pulseTracker]; 147 | self.heartRateLabel.text = [NSString stringWithFormat:@"%.0f BPM", pulseTracker.heartRate]; 148 | self.variabilityLabel.text = [NSString stringWithFormat:@"%d ms", (int) (0.5 + pulseTracker.r2r * 1000)]; 149 | } 150 | 151 | - (void)updateHRStatus { 152 | BTPulseTracker *pulseTracker = [(BTAppDelegate *)[[UIApplication sharedApplication] delegate] pulseTracker]; 153 | self.hrConnectionStatusLabel.text = pulseTracker.connectionStatusWithDuration; 154 | self.hrDataStatusLabel.text = pulseTracker.receivedStatusWithDuration; 155 | } 156 | 157 | - (void)updateUploadStatus { 158 | BTPulseTracker *pulseTracker = [(BTAppDelegate *)[[UIApplication sharedApplication] delegate] pulseTracker]; 159 | self.hrUploadStatusLabel.text = [pulseTracker.uploader getStatus]; 160 | } 161 | 162 | - (IBAction)onToggleHeartLock:(UIButton *)sender { 163 | [sender setSelected:!sender.selected]; 164 | BTPulseTracker *pulseTracker = [(BTAppDelegate *)[[UIApplication sharedApplication] delegate] pulseTracker]; 165 | pulseTracker.connectOnlyToNickname = sender.selected; 166 | } 167 | 168 | @end 169 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoAsset.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoAsset.h 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 2/5/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface BTPhotoAsset : NSObject 13 | 14 | 15 | // Unique asset URL as supplied by assets library framework. Persists through restart. 16 | // Always set. 17 | @property (strong) NSString *assetURL; 18 | 19 | 20 | // https://fluxtream.atlassian.net/wiki/display/FLX/BodyTrack+server+APIs#BodyTrackserverAPIs-/photoUpload?connector_name=CONNECTOR_NAME 21 | 22 | // Fluxtream's facet ID; used for setting comment or tags 23 | // "" means never uploaded 24 | // Otherwise, stores "id" returned in a successful Fluxtream upload 25 | 26 | @property (strong) NSString *facetID; 27 | 28 | // "0" means never uploaded 29 | // "1" we want to upload 30 | // Otherwise, stores the "key" returned by fluxtream upload, e.g. 31 | // "14.FluxtreamCapture.photo.2013098.1365434963721_e1adc5810563c2ac4c501aeab414b5c6f41a32b6e447ce49408c742506b8c300" 32 | // Used for fetching image, e.g. /photo/ID 33 | 34 | // The UID is the very first number, up to the ".", and we need it for uploading metadata. 35 | 36 | @property (strong) NSString *uploadStatus; 37 | 38 | // "" if no comment 39 | // Otherwise, has the most recent user input 40 | @property (strong) NSString *comment; 41 | 42 | // "" if no tags 43 | // Otherwise, tags are delimited by ",". Alphanumerics, dashes, and underscores are allowed. All else is converted to underscore at the server 44 | 45 | @property (strong) NSString *tags; 46 | 47 | // True if comment or tags needs uploading 48 | 49 | @property (assign) BOOL commentNeedsUpdate; 50 | 51 | - (id)initWithAssetURL:(NSString *)assetURL uploadStatus:(NSString *)uploadStatus; 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoAsset.m: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoAsset.m 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 2/5/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import "BTPhotoAsset.h" 10 | 11 | @implementation BTPhotoAsset 12 | 13 | - (id)initWithAssetURL:(NSString *)assetURL uploadStatus:(NSString *)uploadStatus 14 | { 15 | if (self = [super init]) 16 | { 17 | _assetURL = assetURL; 18 | _facetID = @""; 19 | _uploadStatus = uploadStatus; 20 | _comment = @""; 21 | _tags = @""; 22 | _commentNeedsUpdate = NO; 23 | } 24 | 25 | return self; 26 | } 27 | 28 | - (NSString *)description 29 | { 30 | return [NSString stringWithFormat:@"%@ / %@ / %@ / %@ / %@", [[self uploadStatus] substringFromIndex:0], [self assetURL], [self facetID], [self comment], [self tags]]; 31 | } 32 | 33 | #pragma mark - NSCoding 34 | 35 | - (void)encodeWithCoder:(NSCoder *)aCoder 36 | { 37 | [aCoder encodeObject:_assetURL forKey:@"assetURL"]; 38 | [aCoder encodeObject:_facetID forKey:@"facetID"]; 39 | [aCoder encodeObject:_uploadStatus forKey:@"uploadStatus"]; 40 | [aCoder encodeObject:_comment forKey:@"comment"]; 41 | [aCoder encodeObject:_tags forKey:@"tags"]; 42 | [aCoder encodeBool:_commentNeedsUpdate forKey:@"commentNeedsUpdate"]; 43 | } 44 | 45 | 46 | - (id)initWithCoder:(NSCoder *)aDecoder 47 | { 48 | self = [super init]; 49 | 50 | if (self) { 51 | [self setAssetURL:[aDecoder decodeObjectForKey:@"assetURL"]]; 52 | [self setFacetID:[aDecoder decodeObjectForKey:@"facetID"]]; 53 | [self setUploadStatus:[aDecoder decodeObjectForKey:@"uploadStatus"]]; 54 | [self setComment:[aDecoder decodeObjectForKey:@"comment"]]; 55 | [self setTags:[aDecoder decodeObjectForKey:@"tags"]]; 56 | [self setCommentNeedsUpdate:[aDecoder decodeBoolForKey:@"commentNeedsUpdate"]]; 57 | } 58 | 59 | return self; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoDetailCommentViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoDetailCommentViewController.h 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 2/27/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface BTPhotoDetailCommentViewController : UIViewController 12 | 13 | @property (nonatomic, assign) id delegate; 14 | 15 | @property (strong, nonatomic) IBOutlet UITextField *tagEntryView; 16 | @property (strong, nonatomic) IBOutlet UITextView *commentEntryView; 17 | @property (strong, nonatomic) IBOutlet UIBarButtonItem *saveButton; 18 | @property (strong, nonatomic) IBOutlet UIBarButtonItem *cancelButton; 19 | 20 | @property NSString *comment; 21 | @property NSString *tags; 22 | 23 | - (IBAction)saveButtonTapped:(id)sender; 24 | - (IBAction)cancelButtonTapped:(id)sender; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoDetailCommentViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoDetailCommentViewController.m 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 2/27/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import "BTPhotoDetailCommentViewController.h" 10 | #import "BTPhotoDetailViewController.h" 11 | 12 | @interface BTPhotoDetailCommentViewController () 13 | 14 | @end 15 | 16 | @implementation BTPhotoDetailCommentViewController 17 | 18 | - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 19 | { 20 | self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 21 | if (self) { 22 | // Custom initialization 23 | } 24 | return self; 25 | } 26 | 27 | - (void)viewDidLoad 28 | { 29 | [super viewDidLoad]; 30 | [_tagEntryView setText:_tags]; 31 | [_commentEntryView setText:_comment]; 32 | } 33 | 34 | - (void)didReceiveMemoryWarning 35 | { 36 | [super didReceiveMemoryWarning]; 37 | // Dispose of any resources that can be recreated. 38 | } 39 | 40 | - (void)viewDidUnload { 41 | [self setSaveButton:nil]; 42 | [self setCancelButton:nil]; 43 | [super viewDidUnload]; 44 | } 45 | 46 | 47 | 48 | #pragma mark - UITextViewDelegate methods 49 | 50 | - (void)textViewDidEndEditing:(UITextView *)textView 51 | { 52 | [textView resignFirstResponder]; 53 | } 54 | 55 | - (IBAction)saveButtonTapped:(id)sender 56 | { 57 | _comment = [NSString stringWithString:_commentEntryView.text]; 58 | _tags = [NSString stringWithString:_tagEntryView.text]; 59 | 60 | [_delegate updateAnnotationsWithComment:_comment tags:_tags]; 61 | [self dismissViewControllerAnimated:YES completion:nil]; 62 | } 63 | 64 | - (IBAction)cancelButtonTapped:(id)sender 65 | { 66 | [self dismissViewControllerAnimated:YES completion:nil]; 67 | } 68 | @end 69 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoDetailViewController.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | #import "BTPhotoAsset.h" 4 | 5 | @interface BTPhotoDetailViewController : UIViewController 6 | 7 | @property (strong, nonatomic) IBOutlet UIImageView *imageView; 8 | @property (strong, nonatomic) IBOutlet UITextView *metadataView; 9 | @property (strong, nonatomic) IBOutlet UINavigationItem *photoDate; 10 | @property (strong) BTPhotoAsset *photoAsset; 11 | @property (strong, nonatomic) id image; 12 | 13 | - (void)updateAnnotationsWithComment:(NSString *)comment tags:(NSString *)tags; 14 | 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoDetailViewController.mm: -------------------------------------------------------------------------------- 1 | 2 | #import "BTPhotoDetailViewController.h" 3 | #import "BTPhotoDetailCommentViewController.h" 4 | #import "BTPhotoUploader.h" 5 | #import "BTPhotoMetadataRequest.h" 6 | #import "BTPhotoTagsForUserRequest.h" 7 | #import 8 | 9 | @interface BTPhotoDetailViewController () 10 | 11 | @end 12 | 13 | @implementation BTPhotoDetailViewController 14 | 15 | - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 16 | { 17 | self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 18 | if (self) { 19 | // 20 | } 21 | return self; 22 | } 23 | 24 | - (void)viewDidLoad 25 | { 26 | [super viewDidLoad]; 27 | 28 | _metadataView.layer.shadowColor = [[UIColor darkGrayColor] CGColor]; 29 | _metadataView.layer.shadowOffset = CGSizeMake(1.0f, 1.0f); 30 | _metadataView.layer.shadowOpacity = 1.0f; 31 | _metadataView.layer.shadowRadius = 1.0f; 32 | 33 | _imageView.image = _image; 34 | _metadataView.text = [self captionWithTags:_photoAsset.tags comment:_photoAsset.comment]; 35 | } 36 | 37 | 38 | - (void)didReceiveMemoryWarning 39 | { 40 | [super didReceiveMemoryWarning]; 41 | // Dispose of any resources that can be recreated. 42 | } 43 | 44 | - (void)viewDidUnload { 45 | [self setPhotoDate:nil]; 46 | [self setMetadataView:nil]; 47 | [super viewDidUnload]; 48 | } 49 | 50 | - (NSString *)captionWithTags:(NSString *)tags comment:(NSString *)comment 51 | { 52 | NSString *annotation = @""; 53 | 54 | if (![tags isEqual: @""]) { 55 | annotation = [annotation stringByAppendingString:[NSString stringWithFormat:@"Tags\n%@\n\n", tags]]; 56 | } 57 | 58 | if (![comment isEqual:@""]) { 59 | annotation = [annotation stringByAppendingString:[NSString stringWithFormat:@"Comment\n%@", comment]]; 60 | } 61 | 62 | return annotation; 63 | } 64 | 65 | #pragma mark - Storyboards 66 | 67 | -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{ 68 | if ([[segue identifier] isEqualToString:@"commentSegue"]) { 69 | BTPhotoDetailCommentViewController *commentViewController = [segue destinationViewController]; 70 | [commentViewController setDelegate:self]; 71 | commentViewController.comment = _photoAsset.comment; 72 | commentViewController.tags = _photoAsset.tags; 73 | } 74 | } 75 | 76 | #pragma mark - Comment View Delegate 77 | 78 | - (void)updateAnnotationsWithComment:(NSString *)comment tags:(NSString *)tags 79 | { 80 | [_photoAsset setComment:comment]; 81 | [_photoAsset setTags:tags]; 82 | BTPhotoUploader *photoUploader = [BTPhotoUploader sharedPhotoUploader]; 83 | [photoUploader updateAnnotationsForAsset:_photoAsset]; 84 | 85 | _metadataView.text = [self captionWithTags:tags comment:comment]; 86 | [_metadataView setNeedsDisplay]; 87 | } 88 | 89 | @end 90 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoImageUploadRequest.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoImageUploadRequest.h 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 3/14/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface BTPhotoImageUploadRequest : NSObject 13 | 14 | + (NSURLRequest *)uploadRequestForAsset:(ALAsset *)asset; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoImageUploadRequest.mm: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoImageUploadRequest.m 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 3/14/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import "BTPhotoImageUploadRequest.h" 10 | #import "NSUtils.h" 11 | #import "Constants.h" 12 | 13 | static NSString *const kBoundary = @"b0uNd4rYb0uNd4rYaehrtiffegbib"; 14 | 15 | @implementation BTPhotoImageUploadRequest 16 | 17 | + (NSURLRequest *)uploadRequestForAsset:(ALAsset *)asset 18 | { 19 | ALAssetRepresentation *assetRepresentation = [asset defaultRepresentation]; 20 | 21 | UIImageOrientation orientation = UIImageOrientationUp; 22 | NSNumber *orientationValue = [asset valueForProperty:@"ALAssetPropertyOrientation"]; 23 | 24 | if (orientationValue != nil) { 25 | orientation = (UIImageOrientation)[orientationValue intValue]; 26 | } 27 | 28 | UIImage *image = [UIImage imageWithCGImage:[assetRepresentation fullResolutionImage] scale:1.0 orientation:orientation]; 29 | 30 | NSData *imageData = [NSData dataWithData:UIImageJPEGRepresentation(image, 0.1f)]; 31 | NSDate *imageDate = [asset valueForProperty:ALAssetPropertyDate]; 32 | NSString *separator = [NSString stringWithFormat:@"--%@\r\n", kBoundary]; 33 | NSString *closingBoundary = [NSString stringWithFormat:@"--%@--\r\n", kBoundary]; 34 | NSString *crlf = [NSString stringWithFormat:@"\r\n"]; 35 | 36 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 37 | NSString *baseURL = [defaults objectForKey:DEFAULTS_SERVER]; 38 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: [NSString stringWithFormat:@"http://%@/api/bodytrack/photoUpload?connector_name=fluxtream_capture", baseURL]]]; 39 | 40 | [request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; 41 | [request setHTTPShouldHandleCookies:NO]; 42 | [request setTimeoutInterval:30]; 43 | [request setHTTPMethod:@"POST"]; 44 | [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", kBoundary] forHTTPHeaderField: @"Content-Type"]; 45 | 46 | // post body 47 | NSMutableData *body = [NSMutableData data]; 48 | 49 | // metadata 50 | [body appendData:[separator dataUsingEncoding:NSUTF8StringEncoding]]; 51 | [body appendData:[@"Content-Disposition: form-data; name=\"metadata\"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; 52 | NSTimeInterval dateTimeOriginal = [imageDate timeIntervalSince1970]; 53 | [body appendData:[[NSString stringWithFormat:@"{\"capture_time_secs_utc\":%.3f}", dateTimeOriginal] dataUsingEncoding:NSUTF8StringEncoding]]; 54 | [body appendData:[crlf dataUsingEncoding:NSUTF8StringEncoding]]; 55 | 56 | // photo 57 | [body appendData:[separator dataUsingEncoding:NSUTF8StringEncoding]]; 58 | [body appendData:[@"Content-Disposition: form-data; name=\"photo\"; filename=\"photo.jpg\"" dataUsingEncoding:NSUTF8StringEncoding]]; 59 | [body appendData:[crlf dataUsingEncoding:NSUTF8StringEncoding]]; 60 | [body appendData:[@"Content-Type: image/jpeg" dataUsingEncoding:NSUTF8StringEncoding]]; 61 | [body appendData:[crlf dataUsingEncoding:NSUTF8StringEncoding]]; 62 | [body appendData:[crlf dataUsingEncoding:NSUTF8StringEncoding]]; 63 | [body appendData:imageData]; 64 | [body appendData:[crlf dataUsingEncoding:NSUTF8StringEncoding]]; 65 | 66 | // closing boundary 67 | [body appendData:[closingBoundary dataUsingEncoding:NSUTF8StringEncoding]]; 68 | 69 | [request setHTTPBody:body]; 70 | [request addValue:[NSString stringWithFormat:@"%lu", (unsigned long)[body length]] forHTTPHeaderField:@"Content-Length"]; 71 | 72 | NSString *authStr = [NSString stringWithFormat:@"%@:%@", [defaults valueForKey:DEFAULTS_USERNAME], [defaults valueForKey:DEFAULTS_PASSWORD]]; 73 | 74 | NSData *authData = [authStr dataUsingEncoding:NSUTF8StringEncoding]; 75 | NSString *authValue = [NSString stringWithFormat:@"Basic %@", base64encode(authData)]; 76 | [request setValue:authValue forHTTPHeaderField:@"Authorization"]; 77 | 78 | return request; 79 | } 80 | 81 | @end 82 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoMetadataRequest.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoMetadataRequest.h 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 3/15/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "BTPhotoAsset.h" 11 | 12 | @interface BTPhotoMetadataRequest : NSObject 13 | 14 | + (NSURLRequest *)metadataRequestForAsset:(BTPhotoAsset *)asset; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoMetadataRequest.mm: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoMetadataRequest.m 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 3/15/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import "BTPhotoMetadataRequest.h" 10 | #import "Constants.h" 11 | #import "NSUtils.h" 12 | 13 | @implementation BTPhotoMetadataRequest 14 | 15 | + (NSURLRequest *)metadataRequestForAsset:(BTPhotoAsset *)asset 16 | { 17 | if ([asset.facetID isEqual: @""]) { 18 | return nil; 19 | } else { 20 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 21 | NSString *baseURL = [defaults objectForKey:DEFAULTS_SERVER]; 22 | NSArray *keyParts = [asset.uploadStatus componentsSeparatedByString:@"."]; //UID is the first item in the returned array 23 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: [NSString stringWithFormat:@"http://%@/api/bodytrack/metadata/%@/FluxtreamCapture.photo/%@/get", baseURL, [keyParts objectAtIndex:0], asset.facetID]]]; 24 | 25 | [request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; 26 | [request setHTTPShouldHandleCookies:NO]; 27 | [request setTimeoutInterval:30]; 28 | [request setHTTPMethod:@"GET"]; 29 | 30 | NSString *authStr = [NSString stringWithFormat:@"%@:%@", [defaults valueForKey:DEFAULTS_USERNAME], [defaults valueForKey:DEFAULTS_PASSWORD]]; 31 | 32 | NSData *authData = [authStr dataUsingEncoding:NSUTF8StringEncoding]; 33 | NSString *authValue = [NSString stringWithFormat:@"Basic %@", base64encode(authData)]; 34 | [request setValue:authValue forHTTPHeaderField:@"Authorization"]; 35 | 36 | return request; 37 | } 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoMetadataUploadRequest.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoMetadataUploadRequest.h 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 3/14/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "BTPhotoAsset.h" 11 | 12 | @interface BTPhotoMetadataUploadRequest : NSObject 13 | 14 | + (NSURLRequest *)metadataUploadRequestForAsset:(BTPhotoAsset *)asset; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoMetadataUploadRequest.mm: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoMetadataUploadRequest.m 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 3/14/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import "BTPhotoMetadataUploadRequest.h" 10 | #import "Constants.h" 11 | #import "NSUtils.h" 12 | 13 | @implementation BTPhotoMetadataUploadRequest 14 | 15 | + (NSURLRequest *)metadataUploadRequestForAsset:(BTPhotoAsset *)asset 16 | { 17 | // check we have a facet id for the asset before we begin, if not return nil 18 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 19 | NSString *baseURL = [defaults objectForKey:DEFAULTS_SERVER]; 20 | NSArray *keyParts = [asset.uploadStatus componentsSeparatedByString:@"."]; //UID is the first item in the returned array 21 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: [NSString stringWithFormat:@"http://%@/api/bodytrack/metadata/%@/FluxtreamCapture.photo/%@/set", baseURL, [keyParts objectAtIndex:0], asset.facetID]]]; 22 | 23 | [request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; 24 | [request setHTTPShouldHandleCookies:NO]; 25 | [request setTimeoutInterval:30]; 26 | [request setHTTPMethod:@"POST"]; 27 | [request setValue:[NSString stringWithFormat:@"application/x-www-form-urlencoded"] forHTTPHeaderField: @"Content-Type"]; 28 | 29 | NSString *body = [NSString stringWithFormat:@"comment=%@&tags=%@", asset.comment, asset.tags]; 30 | 31 | [request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]]; 32 | [request addValue:[NSString stringWithFormat:@"%lu", (unsigned long)[body length]] forHTTPHeaderField:@"Content-Length"]; 33 | 34 | NSString *authStr = [NSString stringWithFormat:@"%@:%@", [defaults valueForKey:DEFAULTS_USERNAME], [defaults valueForKey:DEFAULTS_PASSWORD]]; 35 | 36 | NSData *authData = [authStr dataUsingEncoding:NSUTF8StringEncoding]; 37 | NSString *authValue = [NSString stringWithFormat:@"Basic %@", base64encode(authData)]; 38 | [request setValue:authValue forHTTPHeaderField:@"Authorization"]; 39 | 40 | return request; 41 | } 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoTagsForUserRequest.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoTagsForUserRequest.h 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 3/26/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "BTPhotoAsset.h" 11 | 12 | @interface BTPhotoTagsForUserRequest : NSObject 13 | 14 | + (NSURLRequest *)allPhotoTagsForUser:(NSString *)uid; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoTagsForUserRequest.mm: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoTagsForUserRequest.m 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 3/26/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import "BTPhotoTagsForUserRequest.h" 10 | #import "Constants.h" 11 | #import "NSUtils.h" 12 | 13 | @implementation BTPhotoTagsForUserRequest 14 | 15 | + (NSURLRequest *)allPhotoTagsForUser:(NSString *)uid 16 | { 17 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 18 | NSString *baseURL = [defaults objectForKey:DEFAULTS_SERVER]; 19 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: [NSString stringWithFormat:@"http://%@/api/bodytrack/users/%@/tags", baseURL, uid]]]; 20 | 21 | [request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; 22 | [request setHTTPShouldHandleCookies:NO]; 23 | [request setTimeoutInterval:30]; 24 | [request setHTTPMethod:@"GET"]; 25 | 26 | NSString *authStr = [NSString stringWithFormat:@"%@:%@", [defaults valueForKey:DEFAULTS_USERNAME], [defaults valueForKey:DEFAULTS_PASSWORD]]; 27 | 28 | NSData *authData = [authStr dataUsingEncoding:NSUTF8StringEncoding]; 29 | NSString *authValue = [NSString stringWithFormat:@"Basic %@", base64encode(authData)]; 30 | [request setValue:authValue forHTTPHeaderField:@"Authorization"]; 31 | 32 | return request; 33 | } 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoUploader.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoUploader.h 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 2/4/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "BTPhotoAsset.h" 12 | 13 | @interface BTPhotoUploader : NSObject 14 | 15 | @property (strong, nonatomic) ALAssetsLibrary *library; 16 | @property (strong) NSMutableArray *photos; 17 | @property (strong) NSMutableArray *discoveredPhotos; 18 | @property (strong) NSArray *oldPhotos; 19 | @property (strong) NSURLRequest *postRequest; 20 | @property (strong) NSTimer *uploadKickTimer; 21 | @property (assign) BOOL isUploading; 22 | @property (assign) BOOL isReloading; 23 | 24 | + (id)sharedPhotoUploader; 25 | 26 | - (void)unuploadedPhotosWithOrientation:(ALAssetOrientation)requestedOrientation; 27 | - (void)markPhotosForUpload:(NSArray *)photos; 28 | - (void)updateAnnotationsForAsset:(BTPhotoAsset *)annotatedAsset; 29 | 30 | - (void)uploadNow; 31 | - (BOOL)savePhotosArray; 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotoUploader.mm: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhotoUploader.m 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 2/4/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import "BTPhotoUploader.h" 10 | #import "BTPhotoAsset.h" 11 | #import "BTPhotoImageUploadRequest.h" 12 | #import "BTPhotoMetadataUploadRequest.h" 13 | #import "Constants.h" 14 | #import "NSUtils.h" 15 | 16 | 17 | @implementation BTPhotoUploader 18 | 19 | static NSString *const kBoundary = @"b0uNd4rYb0uNd4rYaehrtiffegbib"; 20 | 21 | + (id)sharedPhotoUploader 22 | { 23 | static BTPhotoUploader *sharedPhotoUploader = nil; 24 | static dispatch_once_t onceToken; 25 | dispatch_once(&onceToken, ^{ 26 | sharedPhotoUploader = [[self alloc] init]; 27 | }); 28 | 29 | return sharedPhotoUploader; 30 | } 31 | 32 | 33 | - (void)unuploadedPhotosWithOrientation:(ALAssetOrientation)requestedOrientation 34 | { 35 | NSMutableArray *orientedPhotos = [[NSMutableArray alloc] init]; 36 | 37 | void (^assetEnumerator)(ALAsset *, NSUInteger, BOOL *) = ^(ALAsset *result, NSUInteger index, BOOL *stop) { 38 | if (result != NULL) { 39 | ALAssetOrientation orientation = (ALAssetOrientation)[[result valueForProperty:@"ALAssetPropertyOrientation"] intValue]; 40 | 41 | if (orientation == requestedOrientation) { 42 | [orientedPhotos addObject:[[[result defaultRepresentation] url] absoluteString]]; 43 | } 44 | } 45 | }; 46 | 47 | void (^assetGroupEnumerator)(ALAssetsGroup *, BOOL *) = ^(ALAssetsGroup *group, BOOL *stop) { 48 | if (group != nil) { 49 | [group enumerateAssetsUsingBlock:assetEnumerator]; 50 | 51 | NSArray *unuploadedPhotos = [self removeUploadedPhotosFromArray:orientedPhotos]; 52 | 53 | if (unuploadedPhotos) { 54 | [[NSNotificationCenter defaultCenter] postNotificationName:BT_NOTIFICATION_PHOTOS_TO_BE_UPLOADED object:self userInfo:@{@"count":[NSNumber numberWithInt:(int)[unuploadedPhotos count]], @"orientation":[NSNumber numberWithInt:requestedOrientation], @"urls":[NSArray arrayWithArray:unuploadedPhotos]}]; 55 | } 56 | }; 57 | }; 58 | 59 | [_library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:assetGroupEnumerator failureBlock: ^(NSError *error) { 60 | NSLog(@"getting unuploaded photos with orientation failed"); 61 | }]; 62 | } 63 | 64 | - (NSArray *)removeUploadedPhotosFromArray:(NSMutableArray *)orientedPhotos 65 | { 66 | NSMutableArray *unuploadedPhotos = [[NSMutableArray alloc] init]; 67 | 68 | for (NSString *url in orientedPhotos) { 69 | for (BTPhotoAsset *oldPhoto in _photos) { 70 | if ([url isEqualToString:oldPhoto.assetURL]) { 71 | if (![@"0" isEqualToString:oldPhoto.uploadStatus]) { 72 | // forget it 73 | } else { 74 | [unuploadedPhotos addObject:url]; 75 | } 76 | } 77 | } 78 | } 79 | return unuploadedPhotos; 80 | } 81 | 82 | 83 | - (void)markPhotosForUpload:(NSArray *)urls 84 | { 85 | for (NSString *url in urls) { 86 | for (BTPhotoAsset *oldPhoto in _photos) { 87 | if ([oldPhoto.assetURL isEqualToString:url] && [oldPhoto.uploadStatus isEqualToString:@"0"]) { 88 | [oldPhoto setUploadStatus:@"1"]; 89 | } 90 | } 91 | } 92 | [self uploadNow]; 93 | } 94 | 95 | - (id)init 96 | { 97 | if ((self = [super init])) { 98 | _isUploading = NO; 99 | _library = [[ALAssetsLibrary alloc] init]; 100 | 101 | // load the saved photo roll details if they exist 102 | _oldPhotos = [NSKeyedUnarchiver unarchiveObjectWithFile:[self photosArrayArchivePath]]; 103 | if (!_oldPhotos) { 104 | NSLog(@"Couldn't load photos.archive on startup"); 105 | } 106 | 107 | [self discoverPhotos]; 108 | 109 | [[NSOperationQueue mainQueue] addOperationWithBlock: ^{ 110 | _uploadKickTimer = [NSTimer scheduledTimerWithTimeInterval:60 target:self selector:@selector(uploadNow) userInfo:nil repeats:YES]; 111 | [_uploadKickTimer fire]; 112 | }]; 113 | 114 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(assetsLibraryChanged:) name:ALAssetsLibraryChangedNotification object:nil]; 115 | } 116 | 117 | return self; 118 | } 119 | 120 | - (void)reconcileCameraRoll 121 | { 122 | if (!_oldPhotos) { 123 | NSLog(@"no photos.archive found"); 124 | } else { 125 | NSMutableArray *cameraRollCopy = [NSMutableArray arrayWithArray:_photos]; 126 | for (BTPhotoAsset *photo in cameraRollCopy) { 127 | for (BTPhotoAsset *oldPhoto in _oldPhotos) { 128 | if ([oldPhoto.assetURL isEqualToString:photo.assetURL]) { 129 | if (oldPhoto.uploadStatus) { 130 | photo.uploadStatus = oldPhoto.uploadStatus; 131 | photo.facetID = oldPhoto.facetID; 132 | photo.comment = oldPhoto.comment; 133 | photo.tags = oldPhoto.tags; 134 | } else { 135 | NSLog(@"Saved upload status was nil: %@", oldPhoto.assetURL); 136 | } 137 | break; 138 | } 139 | } 140 | } 141 | 142 | _photos = [NSMutableArray arrayWithArray:cameraRollCopy]; 143 | } 144 | 145 | _oldPhotos = [NSArray arrayWithArray:_photos]; 146 | [self savePhotosArray]; 147 | [self uploadNow]; 148 | } 149 | 150 | 151 | - (void)discoverPhotos 152 | { 153 | _discoveredPhotos = [[NSMutableArray alloc] init]; 154 | 155 | void (^assetEnumerator)(ALAsset *, NSUInteger, BOOL *) = ^(ALAsset *result, NSUInteger index, BOOL *stop) { 156 | if (result != NULL) { 157 | // check for orientation, set uploadStatus based on that 158 | NSNumber *orientationValue = [result valueForProperty:@"ALAssetPropertyOrientation"]; 159 | int orientation = 0; 160 | if (orientationValue != nil) { 161 | orientation = (ALAssetOrientation)[orientationValue intValue]; 162 | } 163 | 164 | switch (orientation) { 165 | case ALAssetOrientationUp: 166 | case ALAssetOrientationUpMirrored: 167 | [self processAsset:result forOrientation:DEFAULTS_PHOTO_ORIENTATION_LANDSCAPE_LEFT]; 168 | break; 169 | 170 | case ALAssetOrientationDown: 171 | case ALAssetOrientationDownMirrored: 172 | [self processAsset:result forOrientation:DEFAULTS_PHOTO_ORIENTATION_LANDSCAPE_RIGHT]; 173 | break; 174 | 175 | case ALAssetOrientationLeft: 176 | case ALAssetOrientationLeftMirrored: 177 | [self processAsset:result forOrientation:DEFAULTS_PHOTO_ORIENTATION_UPSIDE_DOWN]; 178 | break; 179 | 180 | case ALAssetOrientationRight: 181 | case ALAssetOrientationRightMirrored: 182 | [self processAsset:result forOrientation:DEFAULTS_PHOTO_ORIENTATION_PORTRAIT]; 183 | break; 184 | 185 | default: 186 | NSLog(@"** Unknown photo orientation! **"); 187 | break; 188 | } 189 | } 190 | }; 191 | 192 | void (^assetGroupEnumerator)(ALAssetsGroup *, BOOL *) = ^(ALAssetsGroup *group, BOOL *stop) { 193 | if (group != nil) { 194 | [group enumerateAssetsUsingBlock:assetEnumerator]; 195 | _photos = [NSMutableArray arrayWithArray:_discoveredPhotos]; 196 | [self reconcileCameraRoll]; 197 | _isReloading = NO; 198 | }; 199 | }; 200 | 201 | [_library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:assetGroupEnumerator failureBlock: ^(NSError *error) { 202 | NSLog(@"discoverPhotos Failure"); 203 | }]; 204 | } 205 | 206 | - (void)processAsset:(ALAsset *)asset forOrientation:(NSString *)orientationKey 207 | { 208 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 209 | BOOL upload = [defaults boolForKey:orientationKey]; 210 | NSString *status = [NSString stringWithFormat:@"%@", (upload ? @"1" : @"0")]; 211 | NSDate *cutoffDate = [defaults objectForKey:DEFAULTS_PHOTO_ORIENTATION_SETTINGS_CHANGED]; 212 | NSDate *assetDate = [asset valueForProperty:ALAssetPropertyDate]; 213 | 214 | if ([assetDate compare:cutoffDate] == NSOrderedAscending) { // asset date is earlier than the last time the orientation settings were changed 215 | status = @"0"; // so don't mark it for upload 216 | } 217 | 218 | BTPhotoAsset *photoAsset = [[BTPhotoAsset alloc] initWithAssetURL:[[[asset defaultRepresentation] url] absoluteString] uploadStatus:status]; 219 | [_discoveredPhotos addObject:photoAsset]; 220 | } 221 | 222 | - (void)uploadNow 223 | { 224 | if (_isUploading == YES) return; 225 | if ([_photos count] > 0) { 226 | _isUploading = YES; 227 | NSLog(@"photoUploader uploadNow"); 228 | 229 | long i = [self photoIndexForUpload]; 230 | 231 | if (i == NSNotFound) { 232 | NSLog(@"No photos marked for upload"); 233 | _isUploading = NO; 234 | } else { 235 | BTPhotoAsset *photoAsset = [_photos objectAtIndex:i]; 236 | [_library assetForURL:[NSURL URLWithString:photoAsset.assetURL] resultBlock:^(ALAsset *asset) { 237 | // force authentication 238 | NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 239 | NSArray *cookies = [cookieStorage cookies]; 240 | for (NSHTTPCookie *cookie in cookies) { 241 | [cookieStorage deleteCookie:cookie]; 242 | } 243 | 244 | NSURLRequest *request; 245 | 246 | if ([photoAsset.uploadStatus isEqual: @"1"]) { 247 | NSLog(@"sending upload request for photo"); 248 | request = [BTPhotoImageUploadRequest uploadRequestForAsset:asset]; 249 | } else { 250 | NSLog(@"sending upload request for metadata"); 251 | request = [BTPhotoMetadataUploadRequest metadataUploadRequestForAsset:photoAsset]; 252 | } 253 | 254 | NSLog(@"%@", request); 255 | 256 | [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { 257 | 258 | if (error) { 259 | NSLog(@"photo upload error code: %ld", (long)[error code]); 260 | switch ([error code]) { 261 | case NSURLErrorUserCancelledAuthentication: 262 | case NSURLErrorUserAuthenticationRequired: 263 | { 264 | UIAlertView *message = [[UIAlertView alloc] initWithTitle:@"Auth Error" 265 | message:@"Check credentials in Settings tab" 266 | delegate:self 267 | cancelButtonTitle:@"OK" 268 | otherButtonTitles:nil]; 269 | [message show]; 270 | [[NSNotificationCenter defaultCenter] postNotificationName:BT_NOTIFICATION_PHOTO_UPLOAD_AUTH_FAILED object:self]; 271 | break; 272 | } 273 | default: 274 | [[NSNotificationCenter defaultCenter] postNotificationName:BT_NOTIFICATION_PHOTO_UPLOAD_NETWORK_ERROR object:self]; 275 | break; 276 | } 277 | _isUploading = NO; 278 | } else { 279 | int statusCode = (int)[(NSHTTPURLResponse *) response statusCode]; 280 | NSLog(@"photo uploader got %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); 281 | NSLog(@"photo upload success: status %d", statusCode); 282 | 283 | NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; 284 | 285 | if ([photoAsset.uploadStatus isEqual:@"1"]) { 286 | NSLog(@"returned from upload request for photo"); 287 | NSString *key = [[json objectForKey:@"payload"] objectForKey:@"key"]; 288 | photoAsset.uploadStatus = key; 289 | NSString *facetID = [[json objectForKey:@"payload"] objectForKey:@"id"]; 290 | photoAsset.facetID = facetID; 291 | } else { 292 | NSLog(@"returned from upload request for metadata"); 293 | if (photoAsset.commentNeedsUpdate == YES) { 294 | [photoAsset setCommentNeedsUpdate:NO]; 295 | } 296 | } 297 | 298 | [_photos replaceObjectAtIndex:i withObject:photoAsset]; 299 | [self savePhotosArray]; 300 | [[NSNotificationCenter defaultCenter] postNotificationName:BT_NOTIFICATION_PHOTO_UPLOAD_SUCCEEDED object:self userInfo:@{@"index":[NSNumber numberWithInt:(int)i]}]; 301 | _isUploading = NO; 302 | [self uploadNow]; 303 | } 304 | }]; 305 | } failureBlock:^(NSError *error) { 306 | NSLog(@"error fetching asset: %@", error); 307 | }]; 308 | } 309 | } 310 | } 311 | 312 | 313 | - (long)photoIndexForUpload 314 | { 315 | for (int i = 0; i < [_photos count]; i++) { 316 | BTPhotoAsset *photoAsset = [_photos objectAtIndex:i]; 317 | if ([photoAsset.uploadStatus isEqual:@"1"] || photoAsset.commentNeedsUpdate == YES) { 318 | return i; 319 | } 320 | } 321 | 322 | return NSNotFound; 323 | } 324 | 325 | 326 | - (NSString *)photosArrayArchivePath 327 | { 328 | NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 329 | NSString *documentDirectory = [documentDirectories objectAtIndex:0]; 330 | NSString *path = [documentDirectory stringByAppendingPathComponent:@"photos.archive"]; 331 | return path; 332 | } 333 | 334 | 335 | - (BOOL)savePhotosArray 336 | { 337 | NSString *path = [self photosArrayArchivePath]; 338 | 339 | BOOL success = [NSKeyedArchiver archiveRootObject:_photos toFile:path]; 340 | 341 | if (success == YES) 342 | { 343 | NSError *error = nil; 344 | 345 | NSURL *url = [NSURL fileURLWithPath:path]; 346 | BOOL excludeFromBackup = [url setResourceValue: [NSNumber numberWithBool: YES] 347 | forKey: NSURLIsExcludedFromBackupKey error: &error]; 348 | if(!excludeFromBackup){ 349 | NSLog(@"Error excluding %@ from backup %@", [url lastPathComponent], error); 350 | } 351 | 352 | } 353 | return success; 354 | } 355 | 356 | - (void)dealloc 357 | { 358 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 359 | } 360 | 361 | 362 | #pragma mark - Assets Library Notifications 363 | 364 | - (void)assetsLibraryChanged:(NSNotification *)notification 365 | { 366 | if (!_isReloading) { 367 | _isReloading = YES; 368 | dispatch_async(dispatch_get_main_queue(), ^{ 369 | [self discoverPhotos]; 370 | }); 371 | } 372 | } 373 | 374 | #pragma mark - Comment & Tag upload handling 375 | 376 | - (void)updateAnnotationsForAsset:(BTPhotoAsset *)annotatedAsset 377 | { 378 | for (BTPhotoAsset *asset in _photos) { 379 | if (asset.assetURL == annotatedAsset.assetURL) { 380 | asset.comment = annotatedAsset.comment; 381 | asset.tags = annotatedAsset.tags; 382 | if (![annotatedAsset.facetID isEqual: @""]) { 383 | asset.facetID = annotatedAsset.facetID; 384 | } else { 385 | asset.uploadStatus = @"1"; 386 | } 387 | 388 | asset.commentNeedsUpdate = YES; 389 | } 390 | } 391 | 392 | [self savePhotosArray]; 393 | [self uploadNow]; 394 | } 395 | 396 | @end 397 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotosViewController.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | #import 4 | #import "BTPhotoUploader.h" 5 | #import "BTPhotoAsset.h" 6 | 7 | @interface BTPhotosViewController : UIViewController 8 | 9 | @property (strong, nonatomic) ALAssetsLibrary *library; 10 | @property (strong, nonatomic) ALAssetsGroup *assetsGroup; 11 | @property (strong, nonatomic) IBOutlet UICollectionView *collectionView; 12 | @property (strong, nonatomic) UIImage *selectedImage; 13 | @property (strong, nonatomic) NSString *selectedImageDate; 14 | @property (strong, nonatomic) BTPhotoAsset *selectedPhotoAsset; 15 | 16 | @property (assign) BOOL isReloading; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTPhotosViewController.m: -------------------------------------------------------------------------------- 1 | #import "BTPhotosViewController.h" 2 | #import "BTPhotoDetailViewController.h" 3 | #import "BTPhotoUploader.h" 4 | #import "BTPhotoAsset.h" 5 | #import "BTCommentBadge.h" 6 | #import "Constants.h" 7 | #import 8 | #import 9 | 10 | @interface BTPhotosViewController () 11 | 12 | @end 13 | 14 | @implementation BTPhotosViewController 15 | 16 | - (void)viewDidLoad 17 | { 18 | [super viewDidLoad]; 19 | 20 | _library = [[ALAssetsLibrary alloc] init]; 21 | _assetsGroup = [[ALAssetsGroup alloc] init]; 22 | 23 | [[NSNotificationCenter defaultCenter] addObserver:self 24 | selector:@selector(photoUploadSucceeded:) 25 | name:BT_NOTIFICATION_PHOTO_UPLOAD_SUCCEEDED object:nil]; 26 | 27 | [[NSNotificationCenter defaultCenter] addObserver:self 28 | selector:@selector(applicationForegrounded:) 29 | name:BT_NOTIFICATION_APP_FOREGROUNDED object:nil]; 30 | 31 | if (!_isReloading) { 32 | _isReloading = YES; 33 | [self refreshCollectionView]; 34 | 35 | // Scroll to the most recent photos 36 | dispatch_async(dispatch_get_main_queue(), ^{ 37 | NSIndexPath *path = [NSIndexPath indexPathForRow:([[[BTPhotoUploader sharedPhotoUploader] photos] count] - 1) inSection:0]; 38 | [_collectionView scrollToItemAtIndexPath:path atScrollPosition:UICollectionViewScrollPositionBottom animated:NO]; 39 | }); 40 | } 41 | 42 | // Register collection view cell for reuse 43 | UINib *cellNib = [UINib nibWithNibName:@"BTPhotoCell" bundle:nil]; 44 | [self.collectionView registerNib:cellNib forCellWithReuseIdentifier:@"BTPhotoCell"]; 45 | 46 | UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; 47 | [flowLayout setItemSize:CGSizeMake(100, 132)]; 48 | [flowLayout setScrollDirection:UICollectionViewScrollDirectionVertical]; 49 | [self.collectionView setCollectionViewLayout:flowLayout]; 50 | } 51 | 52 | - (void)viewWillAppear:(BOOL)animated 53 | { 54 | [self refreshCollectionView]; 55 | } 56 | 57 | 58 | - (void)dealloc 59 | { 60 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 61 | } 62 | 63 | 64 | - (void)didReceiveMemoryWarning 65 | { 66 | [super didReceiveMemoryWarning]; 67 | // Dispose of any resources that can be recreated. 68 | } 69 | 70 | 71 | - (void)button1Tapped:(UIButton *)sender 72 | { 73 | UICollectionViewCell *cell = (UICollectionViewCell*)sender.superview.superview; 74 | 75 | UIActivityIndicatorView *button1ActivityIndicator = (UIActivityIndicatorView *)[cell viewWithTag:101]; 76 | [button1ActivityIndicator startAnimating]; 77 | [sender setHidden:YES]; 78 | 79 | NSIndexPath *path = [self.collectionView indexPathForCell:cell]; 80 | 81 | BTPhotoAsset *assetToUpload = [[[BTPhotoUploader sharedPhotoUploader] photos] objectAtIndex:path.row]; 82 | [assetToUpload setUploadStatus:@"1"]; 83 | [[[BTPhotoUploader sharedPhotoUploader] photos] replaceObjectAtIndex:path.row withObject:assetToUpload]; 84 | [[BTPhotoUploader sharedPhotoUploader] uploadNow]; 85 | } 86 | 87 | 88 | #pragma mark - UICollectionView Data Source methods 89 | 90 | - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 91 | { 92 | static NSString *cellIdentifier = @"BTPhotoCell"; 93 | UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath]; 94 | 95 | BTPhotoAsset *photoAsset = [[[BTPhotoUploader sharedPhotoUploader] photos] objectAtIndex:indexPath.row]; 96 | 97 | UIButton *button1 = (UIButton *)[cell viewWithTag:100]; 98 | [button1 addTarget:self action:@selector(button1Tapped:) forControlEvents:UIControlEventTouchUpInside]; 99 | UIActivityIndicatorView *button1ActivityIndicator = (UIActivityIndicatorView *)[cell viewWithTag:101]; 100 | 101 | if ([photoAsset.uploadStatus isEqual:@"1"]) { 102 | // we want to upload 103 | [button1 setHidden:YES]; 104 | [button1ActivityIndicator startAnimating]; 105 | } else if ([photoAsset.uploadStatus isEqual:@"0"]) { 106 | // don't upload 107 | [button1 setTitle:@"Upload" forState:UIControlStateNormal]; 108 | [button1 setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal]; 109 | [button1 setBackgroundColor:[UIColor colorWithRed:229.0f/255.0f green:229.0f/255.0f blue:229.0f/255.0f alpha:1.0]]; 110 | CALayer *btnLayer = [button1 layer]; 111 | [btnLayer setMasksToBounds:YES]; 112 | [btnLayer setCornerRadius:5.0f]; 113 | [btnLayer setBorderWidth:1.0f]; 114 | [btnLayer setBorderColor:[[UIColor lightGrayColor] CGColor]]; 115 | [button1 setHidden:NO]; 116 | [button1ActivityIndicator stopAnimating]; 117 | } else if (photoAsset.uploadStatus != nil) { 118 | // already uploaded 119 | [button1 setTitle:@"Uploaded" forState:UIControlStateNormal]; 120 | [button1 setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal]; 121 | [button1 setBackgroundColor:[UIColor colorWithRed:217.0f/255.0f green:234.0f/255.0f blue:211.0f/255.0f alpha:1.0]]; 122 | CALayer *btnLayer = [button1 layer]; 123 | [btnLayer setMasksToBounds:YES]; 124 | [btnLayer setCornerRadius:5.0f]; 125 | [btnLayer setBorderWidth:1.0f]; 126 | [btnLayer setBorderColor:[[UIColor colorWithRed:169.0f/255.0f green:221.0f/255.0f blue:151.0f/255.0f alpha:1.0] CGColor]]; 127 | [button1 setHidden:NO]; 128 | [button1ActivityIndicator stopAnimating]; 129 | } else { 130 | // should never happen 131 | NSLog(@"Photo uploadStatus was nil!"); 132 | } 133 | 134 | BTCommentBadge *commentBadge = (BTCommentBadge *)[cell viewWithTag:400]; 135 | if ([photoAsset.comment isEqual:@""] && [photoAsset.tags isEqual:@""]) { 136 | [commentBadge setHidden:YES]; 137 | } else { 138 | [commentBadge setHidden:NO]; 139 | } 140 | 141 | [_library assetForURL:[NSURL URLWithString:photoAsset.assetURL] resultBlock:^(ALAsset *asset) { 142 | UIImageView *cellImageView = (UIImageView *)[cell viewWithTag:300]; 143 | UIImage *thumb = [UIImage imageWithCGImage:[asset thumbnail]]; 144 | [cellImageView setImage:thumb]; 145 | } failureBlock:^(NSError *error) { 146 | NSLog(@"error fetching asset: %@", error); 147 | }]; 148 | 149 | return cell; 150 | } 151 | 152 | 153 | - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section 154 | { 155 | return [[[BTPhotoUploader sharedPhotoUploader] photos] count]; 156 | } 157 | 158 | -(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { 159 | return 1; 160 | } 161 | 162 | - (void)refreshCollectionView 163 | { 164 | NSLog(@"Refresh photos collection view"); 165 | [_collectionView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO]; 166 | _isReloading = NO; 167 | } 168 | 169 | 170 | #pragma mark - UICollectionView Delegate methods 171 | 172 | - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath 173 | { 174 | BTPhotoAsset *photoAsset = [[[BTPhotoUploader sharedPhotoUploader] photos] objectAtIndex:indexPath.row]; 175 | [_library assetForURL:[NSURL URLWithString:photoAsset.assetURL] resultBlock:^(ALAsset *asset) { 176 | CGImageRef assetImage = [[asset defaultRepresentation] fullScreenImage]; 177 | _selectedImage = [UIImage imageWithCGImage:assetImage]; 178 | 179 | NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; 180 | [dateFormatter setDateStyle:NSDateFormatterShortStyle]; 181 | [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; 182 | _selectedImageDate = [dateFormatter stringFromDate:[asset valueForProperty:@"ALAssetPropertyDate"]]; 183 | _selectedPhotoAsset = photoAsset; 184 | 185 | [self performSegueWithIdentifier: @"detailSegue" sender:self]; 186 | } failureBlock:^(NSError *error) { 187 | NSLog(@"error fetching asset: %@", error); 188 | }]; 189 | } 190 | 191 | 192 | #pragma mark - Notifications from BTPhotoUploader 193 | 194 | - (void)photoUploadSucceeded:(NSNotification *)notification 195 | { 196 | NSNumber *i = [notification.userInfo objectForKey:@"index"]; 197 | NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[i unsignedIntegerValue] inSection:0]; 198 | 199 | UICollectionViewCell *cell = [_collectionView cellForItemAtIndexPath:indexPath]; 200 | 201 | UIButton *button1 = (UIButton *)[cell viewWithTag:100]; 202 | [button1 setTitle:@"Uploaded" forState:UIControlStateNormal]; 203 | [button1 setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal]; 204 | [button1 setBackgroundColor:[UIColor colorWithRed:217.0f/255.0f green:234.0f/255.0f blue:211.0f/255.0f alpha:1.0]]; 205 | CALayer *btnLayer = [button1 layer]; 206 | [btnLayer setMasksToBounds:YES]; 207 | [btnLayer setCornerRadius:5.0f]; 208 | [btnLayer setBorderWidth:1.0f]; 209 | [btnLayer setBorderColor:[[UIColor colorWithRed:169.0f/255.0f green:221.0f/255.0f blue:151.0f/255.0f alpha:1.0] CGColor]]; 210 | [button1 setHidden:NO]; 211 | 212 | UIActivityIndicatorView *button1ActivityIndicator = (UIActivityIndicatorView *)[cell viewWithTag:101]; 213 | [button1ActivityIndicator stopAnimating]; 214 | } 215 | 216 | 217 | #pragma mark - ALAssetsLibraryChangedNotification 218 | 219 | - (void)assetsLibraryChanged:(NSNotification *)notification 220 | { 221 | if (!_isReloading) { 222 | _isReloading = YES; 223 | [self refreshCollectionView]; 224 | } 225 | } 226 | 227 | - (void)applicationForegrounded:(NSNotification *)notification 228 | { 229 | [self refreshCollectionView]; 230 | } 231 | 232 | 233 | #pragma mark - Storyboards 234 | 235 | -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{ 236 | if ([[segue identifier] isEqualToString:@"detailSegue"]) { 237 | BTPhotoDetailViewController *detailViewController = [segue destinationViewController]; 238 | 239 | detailViewController.image = _selectedImage; 240 | detailViewController.photoDate.title = _selectedImageDate; 241 | detailViewController.photoAsset = _selectedPhotoAsset; 242 | } 243 | } 244 | @end 245 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTSettingsViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTSettingsViewController.h 3 | // 4 | // Created by Rich Henderson on 2/11/13. 5 | // Copyright (c) 2013 BodyTrack. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | @interface BTSettingsViewController : UITableViewController 12 | 13 | // Settings tableview 14 | // Login 15 | @property (strong, nonatomic) IBOutlet UITableViewCell *usernameCell; 16 | @property (strong, nonatomic) IBOutlet UITextField *username; 17 | @property (strong, nonatomic) IBOutlet UITableViewCell *passwordCell; 18 | @property (strong, nonatomic) IBOutlet UITextField *password; 19 | @property (strong, nonatomic) IBOutlet UITableViewCell *serverCell; 20 | @property (strong, nonatomic) IBOutlet UITextField *server; 21 | 22 | // Capture 23 | @property (strong, nonatomic) IBOutlet UITableViewCell *locationCell; 24 | @property (strong, nonatomic) UISwitch *locationSwitch; 25 | @property (strong, nonatomic) IBOutlet UITableViewCell *motionCell; 26 | @property (strong, nonatomic) UISwitch *motionSwitch; 27 | @property (strong, nonatomic) IBOutlet UITableViewCell *appStatsCell; 28 | @property (strong, nonatomic) UISwitch *appStatsSwitch; 29 | @property (strong, nonatomic) IBOutlet UITableViewCell *recordHeartRateCell; 30 | @property (strong, nonatomic) UISwitch *heartRateSwitch; 31 | @property (strong, nonatomic) IBOutlet UITableViewCell *heartbeatSoundCell; 32 | @property (strong, nonatomic) UISwitch *heartbeatSoundSwitch; 33 | 34 | // Automatic Photo Upload 35 | @property (strong, nonatomic) IBOutlet UITableViewCell *portraitCell; 36 | @property (strong, nonatomic) UISwitch *portraitUploadSwitch; 37 | @property (strong, nonatomic) IBOutlet UITableViewCell *upsideDownCell; 38 | @property (strong, nonatomic) UISwitch *upsideDownUploadSwitch; 39 | @property (strong, nonatomic) IBOutlet UITableViewCell *landscapeLeftCell; 40 | @property (strong, nonatomic) UISwitch *landscapeLeftUploadSwitch; 41 | @property (strong, nonatomic) IBOutlet UITableViewCell *landscapeRightCell; 42 | @property (strong, nonatomic) UISwitch *landscapeRightUploadSwitch; 43 | 44 | @property (assign) ALAssetOrientation orientationForUpload; 45 | @property (strong, nonatomic) NSArray *photosForUpload; 46 | 47 | - (IBAction)orientationSettingsChanged:(id)sender; 48 | - (IBAction)serverWillChange:(id)sender; 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /FluxtreamCapture/BTSettingsViewController.mm: -------------------------------------------------------------------------------- 1 | // 2 | // BTSettingsViewController.m 3 | // Stetho 4 | // 5 | // Created by Rich Henderson on 1/7/13. 6 | // Copyright (c) 2013 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import "BTSettingsViewController.h" 10 | #import "BTPulseTracker.h" 11 | #import "BTPhoneTracker.h" 12 | #import "BTAppDelegate.h" 13 | #import "Constants.h" 14 | 15 | #define kPhotosToBeUploaded 1 16 | #define kServerWillChange 2 17 | 18 | @interface BTSettingsViewController () 19 | 20 | @end 21 | 22 | @implementation BTSettingsViewController 23 | 24 | - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 25 | { 26 | self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 27 | if (self) { 28 | // 29 | } 30 | return self; 31 | } 32 | 33 | - (void)configureView 34 | { 35 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 36 | 37 | [_username setText:[defaults objectForKey:DEFAULTS_USERNAME]]; 38 | [_password setText:[defaults objectForKey:DEFAULTS_PASSWORD]]; 39 | [_server setText:[defaults objectForKey:DEFAULTS_SERVER]]; 40 | 41 | _locationSwitch = [[UISwitch alloc] init]; 42 | [_locationSwitch addTarget:self action:@selector(updateFromUI:) forControlEvents:UIControlEventValueChanged]; 43 | [_locationCell setAccessoryView:_locationSwitch]; 44 | [_locationSwitch setOn:[defaults boolForKey:DEFAULTS_RECORD_LOCATION]]; 45 | 46 | _motionSwitch = [[UISwitch alloc] init]; 47 | [_motionSwitch addTarget:self action:@selector(updateFromUI:) forControlEvents:UIControlEventValueChanged]; 48 | [_motionCell setAccessoryView:_motionSwitch]; 49 | [_motionSwitch setOn:[defaults boolForKey:DEFAULTS_RECORD_MOTION]]; 50 | 51 | _appStatsSwitch = [[UISwitch alloc] init]; 52 | [_appStatsSwitch addTarget:self action:@selector(updateFromUI:) forControlEvents:UIControlEventValueChanged]; 53 | [_appStatsCell setAccessoryView:_appStatsSwitch]; 54 | [_appStatsSwitch setOn:[defaults boolForKey:DEFAULTS_RECORD_APP_STATS]]; 55 | 56 | _heartRateSwitch = [[UISwitch alloc] init]; 57 | [_heartRateSwitch addTarget:self action:@selector(updateFromUI:) forControlEvents:UIControlEventValueChanged]; 58 | [_recordHeartRateCell setAccessoryView:_heartRateSwitch]; 59 | [_heartRateSwitch setOn:[defaults boolForKey:DEFAULTS_RECORD_HEARTRATE]]; 60 | 61 | _heartbeatSoundSwitch = [[UISwitch alloc] init]; 62 | [_heartbeatSoundSwitch addTarget:self action:@selector(updateFromUI:) forControlEvents:UIControlEventValueChanged]; 63 | [_heartbeatSoundCell setAccessoryView:_heartbeatSoundSwitch]; 64 | [_heartbeatSoundSwitch setOn:[defaults boolForKey:DEFAULTS_HEARTBEAT_SOUND]]; 65 | 66 | _portraitUploadSwitch = [[UISwitch alloc] init]; 67 | [_portraitUploadSwitch setTag:200]; 68 | [_portraitUploadSwitch addTarget:self action:@selector(updateFromUI:) forControlEvents:UIControlEventValueChanged]; 69 | [_portraitUploadSwitch addTarget:self action:@selector(orientationSettingsChanged:) forControlEvents:UIControlEventValueChanged]; 70 | [_portraitCell setAccessoryView:_portraitUploadSwitch]; 71 | [_portraitUploadSwitch setOn:[defaults boolForKey:DEFAULTS_PHOTO_ORIENTATION_PORTRAIT]]; 72 | 73 | _upsideDownUploadSwitch = [[UISwitch alloc] init]; 74 | [_upsideDownUploadSwitch setTag:201]; 75 | [_upsideDownUploadSwitch addTarget:self action:@selector(updateFromUI:) forControlEvents:UIControlEventValueChanged]; 76 | [_upsideDownUploadSwitch addTarget:self action:@selector(orientationSettingsChanged:) forControlEvents:UIControlEventValueChanged]; 77 | [_upsideDownCell setAccessoryView:_upsideDownUploadSwitch]; 78 | [_upsideDownUploadSwitch setOn:[defaults boolForKey:DEFAULTS_PHOTO_ORIENTATION_UPSIDE_DOWN]]; 79 | 80 | _landscapeLeftUploadSwitch = [[UISwitch alloc] init]; 81 | [_landscapeLeftUploadSwitch setTag:202]; 82 | [_landscapeLeftUploadSwitch addTarget:self action:@selector(updateFromUI:) forControlEvents:UIControlEventValueChanged]; 83 | [_landscapeLeftUploadSwitch addTarget:self action:@selector(orientationSettingsChanged:) forControlEvents:UIControlEventValueChanged]; 84 | [_landscapeLeftCell setAccessoryView:_landscapeLeftUploadSwitch]; 85 | [_landscapeLeftUploadSwitch setOn:[defaults boolForKey:DEFAULTS_PHOTO_ORIENTATION_LANDSCAPE_LEFT]]; 86 | 87 | _landscapeRightUploadSwitch = [[UISwitch alloc] init]; 88 | [_landscapeRightUploadSwitch setTag:203]; 89 | [_landscapeRightUploadSwitch addTarget:self action:@selector(updateFromUI:) forControlEvents:UIControlEventValueChanged]; 90 | [_landscapeRightUploadSwitch addTarget:self action:@selector(orientationSettingsChanged:) forControlEvents:UIControlEventValueChanged]; 91 | [_landscapeRightCell setAccessoryView:_landscapeRightUploadSwitch]; 92 | [_landscapeRightUploadSwitch setOn:[defaults boolForKey:DEFAULTS_PHOTO_ORIENTATION_LANDSCAPE_RIGHT]]; 93 | } 94 | 95 | - (void)viewDidLoad 96 | { 97 | [super viewDidLoad]; 98 | 99 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(photosToBeUploaded:) name:BT_NOTIFICATION_PHOTOS_TO_BE_UPLOADED object:nil]; 100 | [self configureView]; 101 | } 102 | 103 | - (void)didReceiveMemoryWarning 104 | { 105 | [super didReceiveMemoryWarning]; 106 | // Dispose of any resources that can be recreated. 107 | } 108 | 109 | - (void)viewDidUnload { 110 | [super viewDidUnload]; 111 | } 112 | 113 | - (void)dealloc 114 | { 115 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 116 | } 117 | 118 | 119 | - (void)updateUploaderFromUI:(FluxtreamUploaderObjc*)uploader { 120 | if (![uploader.username isEqualToString: _username.text] || 121 | ![uploader.password isEqualToString: _password.text]) { 122 | uploader.username = _username.text; 123 | uploader.password = _password.text; 124 | [uploader uploadNow]; 125 | } 126 | } 127 | 128 | - (IBAction)updateFromUI:(id)sender 129 | { 130 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 131 | [defaults setObject:_username.text forKey:DEFAULTS_USERNAME]; 132 | [defaults setObject:_password.text forKey:DEFAULTS_PASSWORD]; 133 | [defaults setObject:_server.text forKey:DEFAULTS_SERVER]; 134 | [defaults setBool:_locationSwitch.isOn forKey:DEFAULTS_RECORD_LOCATION]; 135 | [defaults setBool:_motionSwitch.isOn forKey:DEFAULTS_RECORD_MOTION]; 136 | [defaults setBool:_appStatsSwitch.isOn forKey:DEFAULTS_RECORD_APP_STATS]; 137 | [defaults setBool:_heartRateSwitch.isOn forKey:DEFAULTS_RECORD_HEARTRATE]; 138 | [defaults setBool:_heartbeatSoundSwitch.isOn forKey:DEFAULTS_HEARTBEAT_SOUND]; 139 | [defaults setBool:_portraitUploadSwitch.isOn forKey:DEFAULTS_PHOTO_ORIENTATION_PORTRAIT]; 140 | [defaults setBool:_upsideDownUploadSwitch.isOn forKey:DEFAULTS_PHOTO_ORIENTATION_UPSIDE_DOWN]; 141 | [defaults setBool:_landscapeLeftUploadSwitch.isOn forKey:DEFAULTS_PHOTO_ORIENTATION_LANDSCAPE_LEFT]; 142 | [defaults setBool:_landscapeRightUploadSwitch.isOn forKey:DEFAULTS_PHOTO_ORIENTATION_LANDSCAPE_RIGHT]; 143 | [[NSUserDefaults standardUserDefaults] synchronize]; 144 | 145 | BTPulseTracker *pulseTracker = [(BTAppDelegate *)[[UIApplication sharedApplication] delegate] pulseTracker]; 146 | pulseTracker.enabled = [defaults boolForKey:DEFAULTS_RECORD_HEARTRATE]; 147 | [self updateUploaderFromUI:pulseTracker.uploader]; 148 | 149 | BTPhoneTracker *phoneTracker = [(BTAppDelegate *)[[UIApplication sharedApplication] delegate] phoneTracker]; 150 | [self updateUploaderFromUI:phoneTracker.batteryUploader]; 151 | [self updateUploaderFromUI:phoneTracker.timeZoneUploader]; 152 | [self updateUploaderFromUI:phoneTracker.appStatsUploader]; 153 | [self updateUploaderFromUI:phoneTracker.locationUploader]; 154 | [self updateUploaderFromUI:phoneTracker.motionUploader]; 155 | 156 | phoneTracker.recordBatteryEnabled = [defaults boolForKey:DEFAULTS_RECORD_APP_STATS]; 157 | phoneTracker.recordAppStatsEnabled = [defaults boolForKey:DEFAULTS_RECORD_APP_STATS]; 158 | phoneTracker.recordLocationEnabled = [defaults boolForKey:DEFAULTS_RECORD_LOCATION]; 159 | phoneTracker.recordMotionEnabled = [defaults boolForKey:DEFAULTS_RECORD_MOTION]; 160 | 161 | 162 | } 163 | 164 | 165 | - (IBAction)orientationSettingsChanged:(id)sender 166 | { 167 | if ([sender isOn]) { 168 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 169 | [defaults setObject:[NSDate date] forKey:DEFAULTS_PHOTO_ORIENTATION_SETTINGS_CHANGED]; 170 | 171 | BTPhotoUploader *photoUploader = [BTPhotoUploader sharedPhotoUploader]; 172 | ALAssetOrientation orientation; 173 | switch ([sender tag]) { 174 | case 200: // Portrait 175 | orientation = ALAssetOrientationRight; 176 | break; 177 | 178 | case 201: // Upside down 179 | orientation = ALAssetOrientationLeft; 180 | break; 181 | 182 | case 202: // Landscape left 183 | orientation = ALAssetOrientationDown; 184 | break; 185 | 186 | case 203: // Landscape right 187 | orientation = ALAssetOrientationUp; 188 | break; 189 | 190 | default: 191 | NSLog(@"orientation not handled"); 192 | orientation = ALAssetOrientationUp; 193 | break; 194 | } 195 | 196 | [photoUploader unuploadedPhotosWithOrientation:orientation]; 197 | } 198 | } 199 | 200 | - (IBAction)serverWillChange:(id)sender 201 | { 202 | NSString *messageBody = @"You shouldn't usually have to change this setting. Are you sure you want to proceed?"; 203 | 204 | UIAlertView *message = [[UIAlertView alloc] initWithTitle:@"Fluxtream Server" 205 | message:messageBody 206 | delegate:self 207 | cancelButtonTitle:@"Cancel" 208 | otherButtonTitles:@"Proceed", nil]; 209 | message.tag = kServerWillChange; 210 | [message show]; 211 | } 212 | 213 | #pragma mark - Photo uploader notifications 214 | 215 | - (void)photosToBeUploaded:(NSNotification *)notification 216 | { 217 | _photosForUpload = [notification.userInfo objectForKey:@"urls"]; 218 | 219 | if ([_photosForUpload count] > 0) { 220 | _orientationForUpload = (ALAssetOrientation)[[notification.userInfo objectForKey:@"orientation"] intValue]; 221 | NSString *messageBody = [NSString stringWithFormat:@"You have %@ existing photos with this orientation. Upload them now?", [notification.userInfo objectForKey:@"count"]]; 222 | 223 | UIAlertView *message = [[UIAlertView alloc] initWithTitle:@"Photo Upload" 224 | message:messageBody 225 | delegate:self 226 | cancelButtonTitle:@"No" 227 | otherButtonTitles:@"Upload", nil]; 228 | message.tag = kPhotosToBeUploaded; 229 | [message show]; 230 | } 231 | 232 | } 233 | 234 | #pragma mark - UIAlertView delegate methods 235 | 236 | - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex 237 | { 238 | // check the tag property on the alertview to determine which of the two possible UIAlertViews we are dealing with 239 | 240 | switch (alertView.tag) { 241 | case kPhotosToBeUploaded: 242 | if (buttonIndex == 1) { 243 | [[BTPhotoUploader sharedPhotoUploader] markPhotosForUpload:_photosForUpload]; 244 | } else { 245 | _photosForUpload = nil; 246 | } 247 | break; 248 | 249 | case kServerWillChange: 250 | if (buttonIndex == 1) { 251 | // the user wants to edit the server - proceed 252 | } else { 253 | // leave it 254 | [_server resignFirstResponder]; 255 | } 256 | 257 | default: 258 | break; 259 | } 260 | 261 | } 262 | 263 | 264 | #pragma mark - UITextField delegate methods 265 | 266 | - (BOOL)textFieldShouldReturn:(UITextField *)textField { 267 | [textField resignFirstResponder]; 268 | [self updateFromUI:self]; 269 | return YES; 270 | } 271 | 272 | @end 273 | -------------------------------------------------------------------------------- /FluxtreamCapture/Constants.h: -------------------------------------------------------------------------------- 1 | // Key names for NSUserDefaults standardUserDefaults 2 | 3 | #define DEFAULTS_USERNAME @"bodytrack_username" 4 | #define DEFAULTS_PASSWORD @"bodytrack_password" 5 | #define DEFAULTS_SERVER @"bodytrack_server" 6 | #define DEFAULTS_FIRSTRUN @"bodytrack_firstrun" 7 | #define DEFAULTS_RECORD_LOCATION @"bodytrack_enable_record_location" 8 | #define DEFAULTS_RECORD_MOTION @"bodytrack_enable_record_motion" 9 | #define DEFAULTS_RECORD_APP_STATS @"bodytrack_enable_record_app_stats" 10 | 11 | #define DEFAULTS_RECORD_HEARTRATE @"bodytrack_enable_record_heartrate" 12 | #define DEFAULTS_HEARTBEAT_SOUND @"bodytrack_enable_heartbeat_sound" 13 | #define DEFAULTS_HEART_NICKNAME @"bodytrack_heart_nickname" 14 | #define DEFAULTS_HEART_CONNECT_ONLY_TO_NICKNAME @"bodytrack_heart_connect_only_to_nickname" 15 | 16 | #define DEFAULTS_PHOTO_ORIENTATION_PORTRAIT @"bodytrack_photo_upload_portrait" 17 | #define DEFAULTS_PHOTO_ORIENTATION_UPSIDE_DOWN @"bodytrack_photo_upload_upside_down" 18 | #define DEFAULTS_PHOTO_ORIENTATION_LANDSCAPE_LEFT @"bodytrack_photo_upload_landscape_left" 19 | #define DEFAULTS_PHOTO_ORIENTATION_LANDSCAPE_RIGHT @"bodytrack_photo_upload_landscape_right" 20 | #define DEFAULTS_PHOTO_ORIENTATION_SETTINGS_CHANGED @"bodytrack_photo_orientation_settings_changed" 21 | 22 | #define BT_NOTIFICATION_PHOTO_UPLOAD_AUTH_FAILED @"bt_photo_upload_auth_failed" 23 | #define BT_NOTIFICATION_PHOTO_UPLOAD_NETWORK_ERROR @"bt_photo_upload_network_error" 24 | #define BT_NOTIFICATION_PHOTO_UPLOAD_SUCCEEDED @"bt_photo_upload_succeeded" 25 | #define BT_NOTIFICATION_APP_FOREGROUNDED @"bt_app_foregrounded" 26 | #define BT_NOTIFICATION_PHOTOS_TO_BE_UPLOADED @"bt_photos_to_be_uploaded" -------------------------------------------------------------------------------- /FluxtreamCapture/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/Default-568h@2x.png -------------------------------------------------------------------------------- /FluxtreamCapture/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/Default.png -------------------------------------------------------------------------------- /FluxtreamCapture/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/Default@2x.png -------------------------------------------------------------------------------- /FluxtreamCapture/FluxtreamCapture-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Fluxtream 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIcons 12 | 13 | CFBundlePrimaryIcon 14 | 15 | CFBundleIconFiles 16 | 17 | appicon.png 18 | appicon@2x.png 19 | 20 | 21 | 22 | CFBundleIdentifier 23 | org.bodytrack.${PRODUCT_NAME:rfc1034identifier} 24 | CFBundleInfoDictionaryVersion 25 | 6.0 26 | CFBundleName 27 | ${PRODUCT_NAME} 28 | CFBundlePackageType 29 | APPL 30 | CFBundleShortVersionString 31 | 1.7.4 32 | CFBundleSignature 33 | ???? 34 | CFBundleVersion 35 | 1.7.4b 36 | LSRequiresIPhoneOS 37 | 38 | UIBackgroundModes 39 | 40 | bluetooth-central 41 | fetch 42 | location 43 | 44 | UIMainStoryboardFile 45 | MainStoryboard_iPhone 46 | UIMainStoryboardFile~ipad 47 | MainStoryboard_iPhone 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UIStatusBarTintParameters 53 | 54 | UINavigationBar 55 | 56 | Style 57 | UIBarStyleDefault 58 | Translucent 59 | 60 | 61 | 62 | UISupportedInterfaceOrientations 63 | 64 | UIInterfaceOrientationPortrait 65 | UIInterfaceOrientationLandscapeLeft 66 | UIInterfaceOrientationLandscapeRight 67 | UIInterfaceOrientationPortraitUpsideDown 68 | 69 | UISupportedInterfaceOrientations~ipad 70 | 71 | UIInterfaceOrientationPortrait 72 | UIInterfaceOrientationPortraitUpsideDown 73 | UIInterfaceOrientationLandscapeLeft 74 | UIInterfaceOrientationLandscapeRight 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /FluxtreamCapture/FluxtreamCapture-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'Stetho' target in the 'Stetho' project 3 | // 4 | 5 | #import 6 | 7 | #ifndef __IPHONE_5_0 8 | #warning "This project uses features only available in iOS SDK 5.0 and later." 9 | #endif 10 | 11 | #ifdef __OBJC__ 12 | #import 13 | #import 14 | #import "TestFlight.h" 15 | #endif 16 | -------------------------------------------------------------------------------- /FluxtreamCapture/TestFlightSDK3.0.2/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | The TestFlight SDK allows you to track how beta testers are testing your application. Out of the box we track simple usage information, such as which tester is using your application, their device model/OS, how long they used the application, and automatic recording of any crashes they encounter. 4 | 5 | The SDK can track more information if you pass it to TestFlight. The Checkpoint API is used to help you track exactly how your testers are using your application. Curious about which users passed level 5 in your game, or posted their high score to Twitter, or found that obscure feature? See "Checkpoint API" down below to see how. 6 | 7 | The SDK also offers a remote logging solution. Find out more about our logging system in the "Remote Logging" section. 8 | 9 | 10 | ## Requirements 11 | 12 | The TestFlight SDK requires iOS 4.3 or above, the Apple LLVM compiler, and the libz library to run. 13 | 14 | 15 | ## Integration 16 | 17 | 1. Add the files to your project: File -> Add Files to " " 18 | 1. Find and select the folder that contains the SDK 19 | 2. Make sure that "Copy items into destination folder (if needed)" is checked 20 | 3. Set Folders to "Create groups for any added folders" 21 | 4. Select all targets that you want to add the SDK to 22 | 23 | 2. Verify that libTestFlight.a has been added to the Link Binary With Libraries Build Phase for the targets you want to use the SDK with 24 | 1. Select your Project in the Project Navigator 25 | 2. Select the target you want to enable the SDK for 26 | 3. Select the Build Phases tab 27 | 4. Open the Link Binary With Libraries Phase 28 | 5. If libTestFlight.a is not listed, drag and drop the library from your Project Navigator to the Link Binary With Libraries area 29 | 6. Repeat Steps 2 - 5 until all targets you want to use the SDK with have the SDK linked 30 | 31 | 3. Add libz to your Link Binary With Libraries Build Phase 32 | 1. Select your Project in the Project Navigator 33 | 2. Select the target you want to enable the SDK for 34 | 3. Select the Build Phases tab 35 | 4. Open the Link Binary With Libraries Phase 36 | 5. Click the + to add a new library 37 | 6. Find libz.dylib in the list and add it 38 | 7. Repeat Steps 2 - 6 until all targets you want to use the SDK with have libz.dylib 39 | 40 | 4. Get your App Token 41 | 42 | 1. If this is a new application, and you have not uploaded it to TestFlight before, first register it here: [https://testflightapp.com/dashboard/applications/create/](https://testflightapp.com/dashboard/applications/create/). 43 | 44 | Otherwise, if you have previously uploaded your app to TestFlight, go to your list of applications ([http://testflightapp.com/dashboard/applications/](http://testflightapp.com/dashboard/applications/)) and click on the application you are using from the list. 45 | 46 | 2. Click on the "App Token" tab on the left. The App Token for that application will be there. 47 | 48 | 5. In your Application Delegate: 49 | 50 | 1. Import TestFlight: `#import "TestFlight.h"` 51 | 52 | 2. Launch TestFlight with your App Token 53 | 54 | In your `-application:didFinishLaunchingWithOptions:` method, call `+[TestFlight takeOff:]` with your App Token. 55 | 56 | -(BOOL)application:(UIApplication *)application 57 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 58 | // start of your application:didFinishLaunchingWithOptions 59 | 60 | [TestFlight takeOff:@"Insert your Application Token here"]; 61 | 62 | // The rest of your application:didFinishLaunchingWithOptions method 63 | // ... 64 | } 65 | 66 | 3. To report crashes to you we install our own uncaught exception handler. If you are not currently using an exception handler of your own then all you need to do is go to the next step. If you currently use an Exception Handler, or you use another framework that does please go to the section on advanced exception handling. 67 | 68 | 69 | ## Uploading your build 70 | 71 | After you have integrated the SDK into your application you need to upload your build to TestFlight. You can upload your build on our [website](https://testflightapp.com/dashboard/builds/add/), using our [desktop app](https://testflightapp.com/desktop/), or by using our [upload API](https://testflightapp.com/api/doc/). 72 | 73 | 74 | ## Basic Features 75 | 76 | ### Session Information 77 | 78 | View information about how often users use your app, how long they use it for, and when they use it. You can see what type of device the user is using, which OS, which language, etc. 79 | 80 | Sessions automatically start at when the app becomes active and end when the app resigns active. Sessions that start shortly after an end continue the session instead of starting a new one. 81 | 82 | NB: Sessions do not start when `takeOff:` is called, `takeOff:` registers callbacks to start sessions when the app is active. 83 | 84 | For **beta** users, you can see who the users are if they have a TestFlight account and their device is registered with TestFlight. 85 | 86 | 87 | ### Crash Reports 88 | 89 | The TestFlight SDK automatically reports all crashes (beta and prod) to TestFlight's website where you can view them. Crash reports are sent **at** crash time. TestFlight will also automatically symbolicate all crashes (if you have uploaded your dSYM). For **beta** apps, on the site, you can see which checkpoints the user passed before the crash and see remote logs that were sent before the crash. 90 | 91 | 92 | ### Beta In App Updates 93 | 94 | If a user is using a **beta** version of your app and that user has permission to install it; an in app popup will ask them if they would like to install the update. If they tap "Install", the new version is installed from inside the app. 95 | 96 | NB: For this to work, you must increment your build version before uploading. Otherwise the new and old builds will have the same version number and we won't know if the user needs to update or is already using the new version. 97 | 98 | To turn this off set this option before calling `takeOff:` 99 | 100 | [TestFlight setOptions:@{ TFOptionDisableInAppUpdates : @YES }]; 101 | 102 | 103 | ## Additional Features 104 | 105 | ### Checkpoints 106 | 107 | When a tester does something you care about in your app, you can pass a checkpoint. For example completing a level, adding a todo item, etc. The checkpoint progress is used to provide insight into how your testers are testing your apps. The passed checkpoints are also attached to crashes, which can help when creating steps to replicate. Checkpoints are visible for all beta builds. 108 | 109 | [TestFlight passCheckpoint:@"CHECKPOINT_NAME"]; 110 | 111 | Use `passCheckpoint:` to track when a user performs certain tasks in your application. This can be useful for making sure testers are hitting all parts of your application, as well as tracking which testers are being thorough. 112 | 113 | Checkpoints are meant to tell you if a user visited a place in your app or completed a task. They should not be used for debugging purposes. Instead, use Remote Logging for debugging information (more information below). 114 | 115 | NB: Checkpoints are only recorded during BETA sessions. 116 | 117 | 118 | ### Custom Environment Information 119 | 120 | In **beta** builds, if you want to see some extra information about your user, you can add some custom environment information. You must add this information before the session starts (a session starts at `takeOff:`) to see it on TestFlight's website. NB: You can only see this information for **beta** users. 121 | 122 | [TestFlight addCustomEnvironmentInformation:@"info" forKey:@"key"]; 123 | 124 | You may call this method as many times as you would like to add more information. 125 | 126 | 127 | ### User Feedback 128 | 129 | In **beta** builds, if you collect feedback from your users, you may pass it back to TestFlight which will associate it with the user's current session. 130 | 131 | [TestFlight submitFeedback:feedback]; 132 | 133 | Once users have submitted feedback from inside of the application you can view it in the feedback area of your build page. 134 | 135 | 136 | ### Remote Logging 137 | 138 | Remote Logging allows you to see the logs your app prints out remotely, on TestFlight's website. You can see logs for **beta sessions**. 139 | 140 | To use it, simply replace all of your `NSLog` calls with `TFLog` calls. An easy way to do this without rewriting all your `NSLog` calls is to add the following macro to your `.pch` file. 141 | 142 | #import "TestFlight.h" 143 | #define NSLog TFLog 144 | 145 | Not only will `TFLog` log remotely to TestFlight, it will also log to the console (viewable in a device's logs) and STDERR (shown while debugging) just like NSLog does, providing a complete replacement. 146 | 147 | For even better information in your remote logs, such as file name and line number, you can use this macro instead: 148 | 149 | #define NSLog(__FORMAT__, ...) TFLog((@"%s [Line %d] " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__) 150 | 151 | Which will produce output that looks like 152 | 153 | -[MyAppDelegate application:didFinishLaunchingWithOptions:] [Line 45] Launched! 154 | 155 | NB: Logs are only recorded during sessions. 156 | 157 | **Custom Logging** 158 | 159 | If you have your own custom logging, call `TFLog` from your custom logging function. If you do not need `TFLog` to log to the console or STDERR because you handle those yourself, you can turn them off with these calls: 160 | 161 | [TestFlight setOptions:@{ TFOptionLogToConsole : @NO }]; 162 | [TestFlight setOptions:@{ TFOptionLogToSTDERR : @NO }]; 163 | 164 | 165 | ## Advanced Notes 166 | 167 | ### Checkpoint API 168 | 169 | When passing a checkpoint, TestFlight logs the checkpoint synchronously (See Remote Logging for more information). If your app has very high performance needs, you can turn the logging off with the `TFOptionLogOnCheckpoint` option. 170 | 171 | 172 | ### Remote Logging 173 | 174 | All logging is done synchronously. Every time the SDK logs, it must write data to a file. This is to ensure log integrity at crash time. Without this, we could not trust logs at crash time. If you have a high performance app, please email support@testflightapp.com for more options. 175 | 176 | 177 | ### Advanced Session Control 178 | 179 | Continuing sessions: You can adjust the amount of time a user can leave the app for and still continue the same session when they come back by changing the `TFOptionSessionKeepAliveTimeout` option. Change it to 0 to turn the feature off. 180 | 181 | Manual Session Control: If your app is a music player that continues to play music in the background, a navigation app that continues to function in the background, or any app where a user is considered to be "using" the app even while the app is not active you should use Manual Session Control. Please only use manual session control if you know exactly what you are doing. There are many pitfalls which can result in bad session duration and counts. See `TestFlight+ManualSessions.h` for more information and instructions. 182 | 183 | 184 | ### Advanced Exception/Signal Handling 185 | 186 | An uncaught exception means that your application is in an unknown state and there is not much that you can do but try and exit gracefully. Our SDK does its best to get the data we collect in this situation to you while it is crashing, but it is designed in such a way that the important act of saving the data occurs in as safe way a way as possible before trying to send anything. If you do use uncaught exception or signal handlers, install your handlers before calling `takeOff:`. Our SDK will then call your handler while ours is running. For example: 187 | 188 | /* 189 | My Apps Custom uncaught exception catcher, we do special stuff here, and TestFlight takes care of the rest 190 | */ 191 | void HandleExceptions(NSException *exception) { 192 | NSLog(@"This is where we save the application data during a exception"); 193 | // Save application data on crash 194 | } 195 | /* 196 | My Apps Custom signal catcher, we do special stuff here, and TestFlight takes care of the rest 197 | */ 198 | void SignalHandler(int sig) { 199 | NSLog(@"This is where we save the application data during a signal"); 200 | // Save application data on crash 201 | } 202 | 203 | -(BOOL)application:(UIApplication *)application 204 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 205 | // installs HandleExceptions as the Uncaught Exception Handler 206 | NSSetUncaughtExceptionHandler(&HandleExceptions); 207 | // create the signal action structure 208 | struct sigaction newSignalAction; 209 | // initialize the signal action structure 210 | memset(&newSignalAction, 0, sizeof(newSignalAction)); 211 | // set SignalHandler as the handler in the signal action structure 212 | newSignalAction.sa_handler = &SignalHandler; 213 | // set SignalHandler as the handlers for SIGABRT, SIGILL and SIGBUS 214 | sigaction(SIGABRT, &newSignalAction, NULL); 215 | sigaction(SIGILL, &newSignalAction, NULL); 216 | sigaction(SIGBUS, &newSignalAction, NULL); 217 | // Call takeOff after install your own unhandled exception and signal handlers 218 | [TestFlight takeOff:@"Insert your Application Token here"]; 219 | // continue with your application initialization 220 | } 221 | 222 | You do not need to add the above code if your application does not use exception handling already. 223 | 224 | -------------------------------------------------------------------------------- /FluxtreamCapture/TestFlightSDK3.0.2/TestFlight+AsyncLogging.h: -------------------------------------------------------------------------------- 1 | // 2 | // TestFlight+AsyncLogging.h 3 | // libTestFlight 4 | // 5 | // Created by Jason Gregori on 2/12/13. 6 | // Copyright (c) 2013 TestFlight. All rights reserved. 7 | // 8 | 9 | /* 10 | 11 | When logging, it is important that logs are written synchronously. In the event of a crash, all logs that happened before the crash are gauranteed to be on disk. If they were written asynchronously and a crash occurs, you might lose some very valuable logs that might have helped fixed the crash. 12 | 13 | However, because TFLog waits until writing to disk is complete, it takes a while. If you have a very high preformance app that can't afford to wait for logs, these functions are for you. 14 | 15 | USE THESE, BUT KNOW YOU RISK LOSING SOME LOGS AT CRASH TIME 16 | 17 | */ 18 | 19 | #import "TestFlight.h" 20 | 21 | 22 | 23 | #if __cplusplus 24 | extern "C" { 25 | #endif 26 | void TFLog_async(NSString *format, ...) __attribute__((format(__NSString__, 1, 2))); 27 | void TFLogv_async(NSString *format, va_list arg_list); 28 | #if __cplusplus 29 | } 30 | #endif -------------------------------------------------------------------------------- /FluxtreamCapture/TestFlightSDK3.0.2/TestFlight+ManualSessions.h: -------------------------------------------------------------------------------- 1 | // 2 | // TestFlight+ManualSessions.h 3 | // libTestFlight 4 | // 5 | // Created by Jason Gregori on 5/16/13. 6 | // Copyright (c) 2013 TestFlight. All rights reserved. 7 | // 8 | 9 | /* 10 | 11 | YOU ARE STRONGLY ADVISED NOT TO USE THESE METHODS unless you know exactly what you are doing. By using these you take on the responsibility of ensuring your session data is reported accurately. 12 | 13 | The way TestFlight normally does sessions is to automatically start them at app launch, app did become active, and app will enter foreground and end them at app will resign active, app did enter background, or app will terminate. 14 | 15 | If your app is a music player that continues to play music in the background, a navigation app that continues to function in the background, or any app where a user is considered to be "using" the app even while the app is not active, this file is for you. 16 | 17 | 18 | Usage 19 | ----- 20 | 21 | 1. Add this file to your project. 22 | 23 | 2. Set the manual sessions option to true **before** calling `takeOff:` 24 | 25 | [TestFlight setOptions:@{ TFOptionManualSessions : @YES }]; 26 | 27 | 3. Use the manually start/end session methods to control you sessions. 28 | 29 | 30 | Pitfalls 31 | -------- 32 | 33 | When using manual sessions in the background, you must always be aware of the fact that iOS may suspend your app at any time without any warning. You must end your session before that happens. If you do not, the session will continue and include all the time the app was suspended in it's duration if the app is brought back from suspension. This will lead to very inaccurate session lengths and counts. 34 | 35 | On app termination: For the most accurate sessions, try to end your session if you know the app is about to terminate. If you do not, the session will still be ended on the next launch, however, it's end time will not be exact. In that case, the end time will be within 30 seconds of the correct time (session information is saved every 30 seconds and when a checkpoint is sent). 36 | 37 | Sessions do not continue across termination if you do not end a session before termination. 38 | 39 | On crashes: Do not worry about ending sessions in the event of a crash. Even manual sessions are automatically ended in the event of a crash. 40 | 41 | Continuing sessions: If a session is started without 30 seconds of the last session ending (and their was no termination between the sessions), the last session will continue instead of a new session starting. This is the case in manual and automatic sessions. You may change the timeout or turn this feature off using the `TFOptionSessionKeepAliveTimeout` option. 42 | 43 | */ 44 | 45 | #import "TestFlight.h" 46 | 47 | 48 | 49 | extern NSString *const TFOptionManualSessions; // Defaults to @NO. Set to @YES before calling `takeOff:` in order to use manual session methods. 50 | 51 | 52 | @interface TestFlight (ManualSessions) 53 | 54 | // these methods are thread safe 55 | + (void)manuallyStartSession; 56 | + (void)manuallyEndSession; 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /FluxtreamCapture/TestFlightSDK3.0.2/TestFlight.h: -------------------------------------------------------------------------------- 1 | // 2 | // TestFlight.h 3 | // libTestFlight 4 | // 5 | // Created by Jonathan Janzen on 06/11/11. 6 | // Copyright 2011 TestFlight. All rights reserved. 7 | 8 | #import 9 | #define TESTFLIGHT_SDK_VERSION @"3.0.2" 10 | #undef TFLog 11 | 12 | #if __cplusplus 13 | extern "C" { 14 | #endif 15 | /* 16 | * Remote Logging 17 | * BETA only 18 | * Note: All Logging is synchronous, see the README for more information. 19 | */ 20 | void TFLog(NSString *format, ...) __attribute__((format(__NSString__, 1, 2))); 21 | void TFLogv(NSString *format, va_list arg_list); 22 | void TFLogPreFormatted(NSString *message); 23 | #if __cplusplus 24 | } 25 | #endif 26 | 27 | /** 28 | * TestFlight object 29 | * All methods are class level 30 | */ 31 | @interface TestFlight : NSObject 32 | 33 | /** 34 | * Add custom environment information 35 | * BETA only 36 | * If you want to track custom information such as a user name from your application you can add it here. 37 | * NB: This information must be added before the session starts, it is recorded only on session start. 38 | * 39 | * @param information A string containing the environment you are storing 40 | * @param key The key to store the information with 41 | */ 42 | + (void)addCustomEnvironmentInformation:(NSString *)information forKey:(NSString*)key; 43 | 44 | 45 | /** 46 | * Sets up TestFlight's infrastructure. 47 | * 48 | * - Saves App Token 49 | * - Starts automatic session management 50 | * - Installs Crash Handlers 51 | * - Kicks off sending of old session data 52 | * 53 | * @param applicationToken Will be the application token for the current application. 54 | * The token for this application can be retrieved by going to https://testflightapp.com/dashboard/applications/ 55 | * selecting this application from the list then selecting SDK. 56 | */ 57 | + (void)takeOff:(NSString *)applicationToken; 58 | 59 | /** 60 | * Sets custom options 61 | * 62 | * @param options NSDictionary containing the options you want to set. Available options are described below at "TestFlight Option Keys" 63 | * 64 | */ 65 | + (void)setOptions:(NSDictionary*)options; 66 | 67 | /** 68 | * Track when a user has passed a checkpoint after the flight has taken off. Eg. passed level 1, posted high score. 69 | * BETA only 70 | * Checkpoints are sent in the background. 71 | * Note: The checkpoint is logged synchronously (See TFLog and TFOptionLogOnCheckpoint for more information). 72 | * 73 | * @param checkpointName The name of the checkpoint, this should be a static string 74 | */ 75 | + (void)passCheckpoint:(NSString *)checkpointName; 76 | 77 | /** 78 | * Submits custom feedback to the site. Sends the data in feedback to the site. This is to be used as the method to submit 79 | * feedback from custom feedback forms. 80 | * BETA only 81 | * 82 | * @param feedback Your users feedback, method does nothing if feedback is nil 83 | */ 84 | + (void)submitFeedback:(NSString*)feedback; 85 | 86 | @end 87 | 88 | 89 | /** 90 | * TestFlight Option Keys 91 | * 92 | * Pass these as keys to the dictionary you pass to +`[TestFlight setOptions:]`. 93 | * The values should be NSNumber BOOLs (`[NSNumber numberWithBool:YES]` or `@YES`) 94 | */ 95 | extern NSString *const TFOptionDisableInAppUpdates; // Defaults to @NO. Setting to @YES, disables the in app update screen shown in BETA apps when there is a new version available on TestFlight. 96 | extern NSString *const TFOptionFlushSecondsInterval; // Defaults to @60. Set to a number. @0 turns off the flush timer. 30 seconds is the minimum flush interval. 97 | extern NSString *const TFOptionLogOnCheckpoint; // Defaults to @YES. Because logging is synchronous, if you have a high preformance app, you might want to turn this off. 98 | extern NSString *const TFOptionLogToConsole; // Defaults to @YES. Prints remote logs to Apple System Log. 99 | extern NSString *const TFOptionLogToSTDERR; // Defaults to @YES. Sends remote logs to STDERR when debugger is attached. 100 | extern NSString *const TFOptionReinstallCrashHandlers; // If set to @YES: Reinstalls crash handlers, to be used if a third party library installs crash handlers overtop of the TestFlight Crash Handlers. 101 | extern NSString *const TFOptionReportCrashes; // Defaults to @YES. If set to @NO, crash handlers are never installed. Must be set **before** calling `takeOff:`. 102 | extern NSString *const TFOptionSendLogOnlyOnCrash; // Defaults to @NO. Setting to @YES stops remote logs from being sent when sessions end. They would only be sent in the event of a crash. 103 | extern NSString *const TFOptionSessionKeepAliveTimeout; // Defaults to @30. This is the amount of time a user can leave the app for and still continue the same session when they come back. If they are away from the app for longer, a new session is created when they come back. Must be a number. Change to @0 to turn off. 104 | 105 | -------------------------------------------------------------------------------- /FluxtreamCapture/TestFlightSDK3.0.2/libTestFlight.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/TestFlightSDK3.0.2/libTestFlight.a -------------------------------------------------------------------------------- /FluxtreamCapture/TestFlightSDK3.0.2/release_notes.md: -------------------------------------------------------------------------------- 1 | ## 3.0.2 - April 10, 2014 2 | 3 | - Got rid of autorelease warning "Object 0x1c83f0 of class NSMachPort autoreleased with no pool in place - just leaking". There wasn't a leak (the object in question was retained for the lifetime of the app), but there was a bug because of a missing autorelease pool. 4 | 5 | ## 3.0.1 - April 9, 2014 6 | 7 | - Stop using NSFileManager's `enumeratoAtURL:…` method. The NSURLDirectoryEnumerator is returns causes crashes on iOS 4. 8 | 9 | ## 3.0.0 - February 18, 2014 10 | 11 | - Remove checkpoints, feedback, and logs from production apps. 12 | 13 | ## 2.2.2 - February 5, 2014 14 | 15 | - Remove `+ (void)setDeviceIdentifier:(NSString *)deviceIdentifer`, it is no longer used 16 | - On start up, if unsent events are found only attempt to send some of them (this is in case a device doesn't have internet for a while and unsent events build up) 17 | - Fix crash if you try to run `TFLog(nil)` (thanks Florian!) 18 | 19 | ## 2.2.1 - January 16, 2014 20 | 21 | - Consolidate both SDK versions into one which removes all access to `ASIdentifierManager` 22 | 23 | ## 2.2 - December 17, 2013 24 | 25 | - Restore In App Updates 26 | - Automatic identification of beta testers 27 | 28 | ## 2.1.3 - November 25, 2013 29 | 30 | - Fix bug in 2.1.2-noadid which caused adid to be collected 31 | 32 | ## 2.1.2 - November 19, 2013 33 | 34 | - Fix for bug that caused events to not get sent properly when using the `TFOptionSessionKeepAliveTimeout` option 35 | - Fix for bug that caused logs that were sent immediately after start session to sometimes not be sent to server 36 | 37 | ## 2.1.1 - October 2, 2013 38 | 39 | - Create sdk version that removes all access to `ASIdentifierManager` 40 | - Add UIDevice's `identifierForVendor` 41 | 42 | ## 2.1 - September 30, 2013 43 | 44 | - Full support for the iPhone 5s’ ARM64 processor while still supporting down to iOS 4.3 45 | 46 | ## 2.0.2 - August 30, 2013 47 | 48 | - Fixed a bug where the sdk would cause an app's CPU usage to rise significantly if the device had no internet connection when the app started 49 | 50 | ## 2.0.1 - August 22, 2013 51 | 52 | - Fixed rare `8badf00d` crash in TFNetworkManager that happened when the app was in the background 53 | 54 | ## 2.0 - August 12, 2013 55 | 56 | Improvements 57 | 58 | - ARC 59 | - All public TestFlight methods may be called from any thread or dispatch_queue 60 | - All public TestFlight methods (except for `TFLog` and `takeOff:`) are asynchronous, so there is never a wait on them 61 | - TestFlight never uses more than 1 network connection at a time 62 | - All network traffic is grouped together, sent at once, and transferred in MessagePack. This results in using less bandwidth and less network calls. 63 | - All network traffic if server is not reachable 64 | - Size of SDK reduced by 70% 65 | - New In App Update UI in an alert with landscape support. Should work for all different types of apps. 66 | - Manual Sessions: You can manually control session start and end. See `TestFlight+ManualSessions.h` for more information 67 | - Combining of back to back sessions. If a session starts less than 30 seconds from the last session which ended, the previous session is continued. You may change the time limit (or turn this off) using the `TFOptionSessionKeepAliveTimeout` option key. 68 | - No longer automatically starts a session on `+takeOff:` in order to support new background modes that might launch an app in the background. 69 | - `TFOptionReportCrashes` option to not install crash handlers 70 | - Remove all calls to `dispatch_get_current_queue`, it is deprecated 71 | 72 | Changes 73 | 74 | - Removed all access to mac address 75 | - Added AdSupport.framework requirement (as a replacement for mac address to get accurate user counts) 76 | - Add format attribute to TFLog to show warnings for wrong format specifiers or not using a format string 77 | - Removed Questions 78 | - Removed Feedback View (along with backtrace option) 79 | 80 | Bug Fixes 81 | 82 | - Fixed addrinfo memory leak 83 | - Fixed possible `-[TFAirTrafficController getNumberOrNilFrom:withKey:]` crash when bad data is received. 84 | - CoreTelephony crash work around: this is a workaround of a iOS bug that causes deallocated instances of `CTTelephonyNetworkInfo` to receive notifications which causes crashes. Core Telephony is used to retrieve the device's mobile carrier. 85 | - Fix bug with crash reporting in iOS 7 86 | 87 | 88 | ## 1.2.4 - February 19, 2013 89 | 90 | - Fixed bug that caused crash reports to sometimes not send immediately (they would be resent later) 91 | 92 | ## 1.2.3 - January 8, 2013 93 | 94 | - Fixed typos in readme 95 | - Fixed bug where logs not sent on crash 96 | - Fixed bug where empty crash files were created (but not sent) 97 | - Cache path to TF's directory so it does not need to be regenerated every time 98 | - Use consts for `setOptions:` 99 | - Updated `setDeviceIdentifier:` comments to make them clearer 100 | - Remove potentially conflicting function name `UIColorFromRGB` 101 | - Fixed crash on bad in app update data 102 | 103 | ## 1.2.2 - December 26, 2012 104 | 105 | - Fix typo in app token error message 106 | 107 | ## 1.2.1 - December 26, 2012 108 | 109 | - The max number of concurrent network connections has been reduced from 4 to 2. 110 | 111 | ##1.2 - November 12, 2012 112 | 113 | * Removed Team Token support. As of version 1.2 takeOff must be called with the Application Token, https://testflightapp.com/dashboard/applications/, choose your application, select SDK, get the Token for this Application. 114 | 115 | ##1.2 BETA 3 - October 11, 2012 116 | 117 | * Added application token support. Application Tokens are currently optional if you do not have one you do not need one 118 | 119 | ##1.2 BETA 2 - October 9, 2012 120 | 121 | * Resolved an instance of close_file being called on a bad file descriptor 122 | 123 | ##1.2 BETA 1 - October 1, 2012 124 | 125 | * Removed support for armv6 126 | * Exception handler now returns instead of raising a SIGTRAP 127 | 128 | ##1.1 - September 13, 2012 129 | 130 | * armv7s and iOS 6 support 131 | * Updated for general release 132 | 133 | ##1.1 BETA 3 - September 12, 2012 134 | 135 | * armv7s slice added to library 136 | * fixed typo for in application updates, inAppUdates changed to inAppUpdates 137 | 138 | ##1.1 BETA 2 - September 6, 2012 139 | 140 | * Re-enabled armv6 support 141 | * Added option to disable in application updates 142 | 143 | ##1.1 BETA 1 - July 13, 2012 144 | 145 | * Added TFLogv to allow for log customizations. Check the README or online docs for more information. 146 | * Added option attachBacktraceToFeedback, which attaches a backtrace to feedback sent from the SDK. For users who use feedback in more than one location in the application. 147 | * Resolved issue where other exception handlers would not be called during an exception. 148 | * SDK now sends the device language for a session. 149 | * Documentation fixes. 150 | * Stability fixes. 151 | 152 | ###1.0 - March 29, 2012 153 | 154 | * Resolved occurrences of exceptions with the message "No background task exists with identifier 0" 155 | 156 | ###1.0 BETA 1 - March 23, 2012 157 | 158 | * Privacy Updates 159 | * UDID is no longer collected by the SDK. During testing please use `[TestFlight setDeviceIdentifier:[[UIDevice currentDevice] uniqueIdentifier]];` to send the UDID so you can identify your testers. For release do not set `+setDeviceIdentifier`. See Beta Testing and Release Differentiation in the README or online at [https://testflightapp.com/sdk/doc/1.0beta1/](http://testflightapp.com/sdk/doc/1.0beta1/) 160 | 161 | ###0.8.3 - February 14, 2012 162 | 163 | * Rolled previous beta code into release builds 164 | * No longer allow in application updates to occur in applications that were obtained from the app store. 165 | 166 | **Tested compiled library with:** 167 | 168 | * Xcode 4.3 169 | * Xcode 4.2 170 | * Xcode 4.1 171 | * Xcode 3.2.6 172 | 173 | ###0.8.3 BETA 5 - February 10, 2012 174 | 175 | * Changed logging from asynchronous to synchronous. 176 | * Resolved crash when looking for a log path failed. 177 | * Added submitFeedback to the TestFlight class to allow for custom feedback forms. 178 | 179 | ###0.8.3 BETA 4 - January 20, 2012 180 | 181 | * Resolved an issue that occured when an application was upgraded from 0.8.3 BETA 1 to 0.8.3 BETA 3+ with unsent data from 0.8.3 BETA 1 182 | 183 | ###0.8.3 BETA 3 - January 19, 2012 184 | 185 | * On crash log files over 64k will not be sent until next launch. 186 | 187 | **Known Issues:** 188 | 189 | * Logging massive amounts of data at the end of a session may prevent the application from launching in time on next launch 190 | 191 | ###0.8.3 BETA 2 - January 13, 2012 192 | 193 | * libz.dylib is now required to be added to your "Link Binary with Libraries" build phase 194 | * Log file compression, The compression is done on an as needed basis rather than before sending 195 | * Changed all outgoing data from JSON to MessagePack 196 | * Added option `logToSTDERR` to disable the `STDERR` logger 197 | 198 | ###0.8.3 BETA 1 - December 29, 2011 199 | 200 | * In rare occurrences old session data that had not been sent to our server may have been discarded or attached to the wrong build. It is now no longer discarded 201 | * Made sending of Session End events more robust 202 | * Network queuing system does better bursting of unsent data 203 | * Log files that are larger than 64K are now sent sometime after the next launch 204 | * Log files that are larger than 16MB are no longer supported and will be replaced with a message indicating the log file was too large 205 | * Fixed crashes while resuming from background 206 | 207 | ###0.8.2 - December 20, 2011 208 | 209 | * Promoted 0.8.2 BETA 4 to stable 210 | 211 | **Known Issues:** 212 | 213 | * Under some circumstances Session End events may not be sent until the next launch. 214 | * With large log files Session End events may take a long time to show up. 215 | 216 | **Tested compiled library with:** 217 | 218 | * Xcode 4.3 219 | * Xcode 4.2 220 | * Xcode 4.1 221 | * Xcode 3.2.6 222 | 223 | ###0.8.2 BETA 4 - December 12, 2011 224 | 225 | * Prevented "The string argument is NULL" from occuring during finishedHandshake in rare cases 226 | * Resolved issue where data recorded while offline may not be sent 227 | 228 | ###0.8.2 BETA 3 - December 8, 2011 229 | 230 | * Added auto-release pools to background setup and tear down 231 | 232 | ###0.8.2 BETA 2 - December 5, 2011 233 | 234 | * Fixed the "pointer being freed was not allocated" bug 235 | 236 | ###0.8.1 - November 18, 2011 237 | 238 | * Implemented TFLog logging system, see README for more information 239 | * Fixed an issue where Session End events may not be sent until next launch 240 | * Fixed an issue where duplicate events could be sent 241 | * Fixed an issue with Session End events not being sent from some iPod touch models 242 | 243 | **Tested compiled library with:** 244 | 245 | * Xcode 4.2 246 | * Xcode 4.1 247 | * Xcode 3.2.6 248 | 249 | ###0.8 - November 8, 2011 250 | 251 | * Added `SIGTRAP` as a signal type that we catch 252 | * Removed all Objective-c from crash reporting 253 | * Removed the use of non signal safe functions from signal handling 254 | * Created a signal safe way to get symbols from a stack trace 255 | * Changed the keyboardType for Long Answer Questions and Feedback to allow for international character input 256 | * Changed `TESTFLIGHT_SDK_VERSION` string to be an `NSString` 257 | * Changed cache folder from Library/Caches/TestFlight to Library/Caches/com.testflight.testflightsdk 258 | * Fixed issue with saving data when device is offline 259 | * Fixed compability issues with iOS 3 260 | * Added calling into the rootViewController shouldAutorotateToInterfaceOrientation if a rootViewController is set 261 | * Made the comments in TestFlight.h compatible with Appledoc 262 | 263 | Tested compiled library with: 264 | 265 | * Xcode 4.2 266 | * Xcode 4.1 267 | * Xcode 3.2 268 | 269 | ###0.7.2 - September 29, 2011 270 | 271 | * Changed `TESTFLIGHT_SDK_VERSION` string to be an `NSString` 272 | * Fixed an issue where exiting an application while the SDK is active caused modal views to be dismissed 273 | 274 | ###0.7.1 - September 22, 2011 275 | 276 | * Internal release 277 | * Refactoring 278 | 279 | ###0.7 - September 21, 2011 280 | 281 | * Moved TestFlight images and data to the Library/Caches folder 282 | * Resolved an issue where sometimes the rootViewController could not be found and feedback, questions and upgrade views would not be displayed 283 | * In application upgrade changed to allow skipping until the next version is installed and allows upgrades to be forced 284 | * Fixed a memory leak when launching questions 285 | 286 | ###0.6 - September 2, 2011 287 | 288 | * Renamed base64_encode to testflight_base64_encode to remove a conflict with other third party libraries 289 | * Added ability to reinstall crash handlers when they are overwritten using the setOptions API 290 | * Fixed an issue where crash reports might not get sent under certain circumstances 291 | * Fixed a deadlock when the application is put in the background and then resumed before all information can be sent 292 | * Fixed an issue when attempting to un-install all signal handlers during a signal 293 | * Added support for landscape mode on the iPad to the Questions and Feedback views 294 | * Crash reporting now works in versions of Xcode earlier than 4.2 295 | * Fixed a memory leak during handshake 296 | 297 | ###0.5 - August 19, 2011 298 | 299 | * Feedback that is not attached to a checkpoint [TestFlight openFeedbackView] 300 | * Usability changes to question views 301 | * Removed pause and resume sessions, replaced with sessions being stopped and started 302 | * Added text auto correction to the Long Answer question type 303 | * Crash reports now send on crash instead of next launch 304 | 305 | ###0.4 - August 15, 2011 306 | 307 | * In Application Feedback with Questions 308 | * In application updates 309 | * Custom Environment Information added 310 | * Networking stack reimplementation 311 | * Exception handling fixes 312 | 313 | ###0.3 - June 15, 2011 314 | 315 | * Removed all mention of JSONKit from the README 316 | * Added support for using both the Bundle Version and the Bundle Short Version string 317 | 318 | ###0.2 - June 14, 2011 319 | 320 | * Removed all categories this allows users to use the SDK without having to set -ObjC and -load_all 321 | * Prefixed JSONKit for use in TestFlight to remove reported issues where some users were already using JSONKit 322 | * Added support for armv6 again 323 | 324 | ###0.1 - June 11, 2011 325 | 326 | * Initial Version 327 | -------------------------------------------------------------------------------- /FluxtreamCapture/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /FluxtreamCapture/locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/locked.png -------------------------------------------------------------------------------- /FluxtreamCapture/main.mm: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Stetho 4 | // 5 | // Created by Nick Winter on 10/20/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "BTAppDelegate.h" 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([BTAppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /FluxtreamCapture/second.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/second.png -------------------------------------------------------------------------------- /FluxtreamCapture/second@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/second@2x.png -------------------------------------------------------------------------------- /FluxtreamCapture/unlocked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/FluxtreamCapture/unlocked.png -------------------------------------------------------------------------------- /FluxtreamUploaderCpp.h: -------------------------------------------------------------------------------- 1 | // 2 | // FluxtreamUploaderCpp.h 3 | // ZeoRelay 4 | // 5 | // Created by rsargent on 10/23/12. 6 | // Copyright (c) 2012 rsargent. All rights reserved. 7 | // 8 | 9 | #ifndef __ZeoRelay__FluxtreamUploaderCpp__ 10 | #define __ZeoRelay__FluxtreamUploaderCpp__ 11 | 12 | #include 13 | 14 | class FluxtreamUploaderCpp { 15 | protected: 16 | void *fluxtreamUploaderObjc; 17 | 18 | public: 19 | FluxtreamUploaderCpp(); 20 | ~FluxtreamUploaderCpp(); 21 | 22 | std::string getUsername(); 23 | void setUsername(const std::string &username); 24 | 25 | void setPassword(const std::string &password); 26 | 27 | void addChannel(const std::string &channelName); 28 | void addSample(double time, double channel0); 29 | void addSample(double time, double channel0, double channel1); 30 | void addSample(double time, double channel0, double channel1, double channel2); 31 | void addSample(double time, double channel0, double channel1, double channel2, double channel3); 32 | void addSample(double time, double channel0, double channel1, double channel2, double channel3, 33 | double channel4); 34 | void addSampleFromArray(double time, double *channels, int channelCount); 35 | 36 | // Samples are queued when added, then uploaded in batches to minimize communication overhead 37 | // and save battery life. The batching behavior can be controlled by setting the 38 | // "maximum upload age" and "maximum upload sample count", below 39 | // 40 | // Maximum upload age is the maximum age a sample can be, in seconds, before 41 | // upload is automatically triggered. Default is 30 seconds. 0 means send everything immediately. 42 | void setMaximumUploadAge(double maxAge); 43 | double getMaximumUploadAge(); 44 | 45 | // Maximum upload sample count is the maximum number of samples that can be uploaded in a single batch. 46 | // (If the queue becomes larger than this count, its contents will be uploaded in multiple batches) 47 | void setMaximumUploadSampleCount(int maximumUploadSampleCount); 48 | int getMaximumUploadSampleCount(); 49 | }; 50 | 51 | 52 | #endif /* defined(__ZeoRelay__FluxtreamUploaderCpp__) */ 53 | -------------------------------------------------------------------------------- /FluxtreamUploaderCpp.mm: -------------------------------------------------------------------------------- 1 | // 2 | // FluxtreamUploaderCpp.mm 3 | // 4 | // Created by rsargent on 10/23/12. 5 | // Copyright (c) 2012 rsargent. All rights reserved. 6 | // 7 | 8 | #include "FluxtreamUploaderCpp.h" 9 | 10 | #import "FluxtreamUploaderObjc.h" 11 | 12 | FluxtreamUploaderCpp::FluxtreamUploaderCpp() { 13 | fluxtreamUploaderObjc = (void*) CFBridgingRetain([[FluxtreamUploaderObjc alloc] init]); 14 | } 15 | 16 | FluxtreamUploaderCpp::~FluxtreamUploaderCpp() { 17 | CFBridgingRelease(fluxtreamUploaderObjc); 18 | fluxtreamUploaderObjc = NULL; 19 | } 20 | 21 | #define getUploader() ((__bridge FluxtreamUploaderObjc*) fluxtreamUploaderObjc) 22 | 23 | std::string FluxtreamUploaderCpp::getUsername() { 24 | return std::string([getUploader().username cStringUsingEncoding:NSUTF8StringEncoding]); 25 | } 26 | 27 | void FluxtreamUploaderCpp::setUsername(const std::string &username) { 28 | getUploader().username = [NSString stringWithUTF8String:username.c_str()]; 29 | } 30 | 31 | void FluxtreamUploaderCpp::setPassword(const std::string &password) { 32 | getUploader().password = [NSString stringWithUTF8String:password.c_str()]; 33 | } 34 | 35 | void FluxtreamUploaderCpp::addChannel(const std::string &channelName) { 36 | [getUploader() addChannel: [NSString stringWithUTF8String: channelName.c_str()]]; 37 | } 38 | 39 | void FluxtreamUploaderCpp::addSample(double time, double ch0) { 40 | [getUploader() addSample:time ch0:ch0]; 41 | } 42 | 43 | void FluxtreamUploaderCpp::addSample(double time, double ch0, double ch1) { 44 | [getUploader() addSample:time ch0:ch0 ch1:ch1]; 45 | } 46 | 47 | void FluxtreamUploaderCpp::addSample(double time, double ch0, double ch1, double ch2) { 48 | [getUploader() addSample:time ch0:ch0 ch1:ch1 ch2:ch2]; 49 | } 50 | 51 | void FluxtreamUploaderCpp::addSample(double time, double ch0, double ch1, double ch2, double ch3) { 52 | [getUploader() addSample:time ch0:ch0 ch1:ch1 ch2:ch2 ch3:ch3]; 53 | } 54 | 55 | void FluxtreamUploaderCpp::addSample(double time, double ch0, double ch1, double ch2, double ch3, double ch4) { 56 | [getUploader() addSample:time ch0:ch0 ch1:ch1 ch2:ch2 ch3:ch3 ch4:ch4]; 57 | } 58 | 59 | // Samples are queued when added, then uploaded in batches to minimize communication overhead 60 | // and save battery life. The batching behavior can be controlled by setting the 61 | // "maximum upload age" and "maximum upload sample count", below 62 | // 63 | // Maximum upload age is the maximum age a sample can be, in seconds, before 64 | // upload is automatically triggered. Default is 30 seconds. 0 means send everything immediately. 65 | void setMaximumUploadAge(double maxAge); 66 | double getMaximumUploadAge(); 67 | 68 | // Maximum upload sample count is the maximum number of samples that can be uploaded in a single batch. 69 | // (If the queue becomes larger than this count, its contents will be uploaded in multiple batches) 70 | void setMaximumUploadSampleCount(int maximumUploadSampleCount); 71 | int getMaximumUploadSampleCount(); 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fluxtream-capture-ios 2 | ===================== 3 | 4 | Fluxtream capture for iOS. Records and uploads: 5 | 6 | - Location 7 | - Motion (10 Hz, acceleration and orientation) 8 | - Photos, with tags and comments 9 | - Heart rate and R-R timings for every heart beat from Polar H7 10 | Heart rate requires hardware with Bluetooth Low Energy (BLE, or Bluetooth Smart) support, such as 11 | - iPhone 4s, 5, or later 12 | - iPad 3, 4, or later 13 | - iPod 4th generation, or later 14 | 15 | More info on heart rate capture, R-R timings, and heart rate variability (HRV) at https://docs.google.com/document/d/1eEdfpfL9Jy9EX9_FvWuDv7pA_mWHkcgZfS6ggxaTZD0/edit 16 | 17 | UPLOADING PHOTOS 18 | 19 | Fluxtream Capture can upload photos from your photo roll on demand, or it can automatically upload some or all as well. If you want automatic upload, you can select which photo orientations to automatically upload: 20 | - Portrait 21 | - Upside-down 22 | - Landscape left 23 | - Landscape right 24 | If you select all orientations, all of your photos will be uploaded. If you only want to upload some and not others, consider turning on automatic upload for two of the four orientations -- this lets you express whether to upload a photo simply by rotating your phone when you take the picture. 25 | 26 | You can add tags or a comment to a photo whenever you like -- either before or after upload is fine. 27 | 28 | BUILDING 29 | 30 | Fluxtream Capture is written in Objective C and C++. Simply open FluxtreamCapture.xcodeproj in Xcode 4.6.2+, connect your iPhone, and hit command-R to build and run on your phone. 31 | 32 | More info on building and distributing the app at the Fluxtream wiki, https://fluxtream.atlassian.net/wiki/display/FLX/Fluxtream+Capture+iOS+Development 33 | -------------------------------------------------------------------------------- /Shared/BTPhoneTracker.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTPhoneTracker.h 3 | // Stetho 4 | // 5 | // Created by rsargent on 12/22/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | #import "FluxtreamUploaderObjc.h" 13 | #import "Logger.h" 14 | 15 | @interface BTPhoneTracker : NSObject 16 | 17 | 18 | @property (strong) FluxtreamUploaderObjc *batteryUploader; 19 | @property (strong) FluxtreamUploaderObjc *timeZoneUploader; 20 | @property (strong) FluxtreamUploaderObjc *appStatsUploader; 21 | @property (strong) FluxtreamUploaderObjc *locationUploader; 22 | @property (strong) FluxtreamUploaderObjc *motionUploader; 23 | 24 | @property (strong) Logger *logger; 25 | 26 | @property BOOL recordLocationEnabled; 27 | @property (nonatomic) BOOL recordMotionEnabled; 28 | @property (nonatomic) BOOL recordBatteryEnabled; 29 | @property (nonatomic) BOOL recordAppStatsEnabled; 30 | 31 | // CLLocationManagerDelegate 32 | 33 | - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations; 34 | - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error; 35 | - (void)locationManager:(CLLocationManager *)manager didFinishDeferredUpdatesWithError:(NSError *)error; 36 | 37 | - (void)locationManagerDidPauseLocationUpdates:(CLLocationManager *)manager; 38 | - (void)locationManagerDidResumeLocationUpdates:(CLLocationManager *)manager; 39 | 40 | - (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading; 41 | // - (BOOL)locationManagerShouldDisplayHeadingCalibration:(CLLocationManager *)manager; 42 | 43 | - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status; 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /Shared/BTPulseTracker.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTPulseTracker.h 3 | // HeartRateMonitor 4 | // 5 | // Randy Sargent and Nick Winter 6 | // 7 | 8 | /* 9 | We could subclass this to BTBlueToothLEPulseTracker if we wanted to do other protocols. 10 | */ 11 | 12 | #import 13 | #if TARGET_OS_IPHONE 14 | #import 15 | #else 16 | #import 17 | #endif 18 | #import "FluxtreamUploaderObjc.h" 19 | #import "Logger.h" 20 | 21 | #include "UUID.h" 22 | 23 | #define BT_NOTIFICATION_PULSE @"bt_pulse_notification" 24 | #define BT_NOTIFICATION_HR_DATA @"bt_hr_data_notification" 25 | 26 | @protocol BTPulseTrackerDelegate; 27 | 28 | @interface BTPulseTracker : NSObject 29 | 30 | @property (strong) FluxtreamUploaderObjc *uploader; 31 | @property (weak) id delegate; /// Delegate receives notifications on peripheral connection changes, as well as pulse changes. 32 | @property (strong) Logger *logger; 33 | 34 | @property (nonatomic) BOOL enabled; 35 | @property (nonatomic) BOOL heartbeatSoundEnabled; 36 | 37 | @property (strong) NSTimer *pulseTimer; 38 | 39 | @property (nonatomic) BOOL connectOnlyToNickname; 40 | @property (nonatomic) NSString *connectNickname; 41 | 42 | typedef enum { 43 | BTPulseTrackerDisabledState = 0, 44 | BTPulseTrackerScanState = 1, 45 | BTPulseTrackerConnectingState = 2, 46 | BTPulseTrackerConnectedState = 3, 47 | BTPulseTrackerStoppedState = 4 48 | } BTPulseTrackerState; 49 | 50 | @property double lastStateChangeTime; 51 | @property (nonatomic) BTPulseTrackerState state; 52 | 53 | @property (readonly) NSString *connectionStatus; 54 | @property (readonly) NSString *connectionStatusWithDuration; 55 | @property (readonly) NSString *receivedStatusWithDuration; 56 | @property (readonly) BOOL connected; 57 | @property (readonly) NSString *peripheralNickname; 58 | 59 | @property (copy) NSString *manufacturer; 60 | 61 | @property (assign) double heartRate; 62 | @property (assign) double r2r; 63 | @property (assign) double lastBeatTime; 64 | @property (assign) BOOL lastBeatTimeValid; 65 | @property double lastHRDataReceived; 66 | 67 | - (BOOL)checkBluetooth; 68 | 69 | @end 70 | 71 | 72 | @protocol BTPulseTrackerDelegate 73 | 74 | - (void)onPulseTrackerNoBluetooth:(BTPulseTracker *)aTracker reason:(NSString *)reason; 75 | - (void)onPulseTrackerConnected:(BTPulseTracker *)aTracker; 76 | - (void)onPulseTrackerDisconnected:(BTPulseTracker *)aTracker; 77 | - (void)onPulse:(BTPulseTracker *)aTracker; 78 | 79 | @end 80 | 81 | 82 | -------------------------------------------------------------------------------- /Shared/ChannelList.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // ChannelNames.cpp 3 | // Stetho 4 | // 5 | // Created by rsargent on 12/19/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #include "ChannelList.h" 10 | 11 | // Not thread-safe; caller must hold a lock on "mutex" 12 | 13 | int ChannelList::createList(const char *lst) { 14 | int idx = (int)lists.size(); 15 | 16 | ids[lst] = idx; 17 | 18 | // Split on comma 19 | std::vector channels; 20 | const char *comma; 21 | while ((comma = strchr(lst, ','))) { 22 | channels.push_back(std::string(lst, comma)); 23 | lst = comma + 1; 24 | } 25 | channels.push_back(lst); 26 | lists.push_back(channels); 27 | return idx; 28 | } 29 | 30 | Mutex ChannelList::mutex; 31 | std::map ChannelList::ids; 32 | std::vector > ChannelList::lists; 33 | 34 | -------------------------------------------------------------------------------- /Shared/ChannelList.h: -------------------------------------------------------------------------------- 1 | // 2 | // ChannelNames.h 3 | // Stetho 4 | // 5 | // Created by rsargent on 12/19/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #ifndef __Stetho__ChannelNames__ 10 | #define __Stetho__ChannelNames__ 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include "Mutex.h" 17 | 18 | class ChannelList { 19 | public: 20 | ChannelList(const char *lst) { 21 | ScopedLock lock(mutex); 22 | std::map::const_iterator i = ids.find(lst); 23 | index = (i == ids.end()) ? createList(lst) : i->second; 24 | } 25 | unsigned int getIndex() const { 26 | return index; 27 | } 28 | unsigned int size() const { 29 | return (int) lists[index].size(); 30 | } 31 | std::string get(unsigned int i) const { 32 | return lists[index][i]; 33 | } 34 | 35 | private: 36 | unsigned int index; 37 | 38 | // Static 39 | static Mutex mutex; 40 | static std::map ids; 41 | static std::vector > lists; 42 | static int createList(const char *lst); 43 | }; 44 | 45 | #endif /* defined(__Stetho__ChannelNames__) */ 46 | -------------------------------------------------------------------------------- /Shared/FluxtreamUploader.h: -------------------------------------------------------------------------------- 1 | // 2 | // FluxtreamUploader.h 3 | // Stetho 4 | // 5 | // Created by rsargent on 12/19/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #ifndef __Stetho__FluxtreamUploader__ 10 | #define __Stetho__FluxtreamUploader__ 11 | 12 | #include 13 | 14 | #include "ChannelList.h" 15 | #include "Mutex.h" 16 | #include "Samples.h" 17 | 18 | class FluxtreamUploader { 19 | public: 20 | 21 | FluxtreamUploader(); 22 | 23 | void setUsername(const char *username); 24 | void setPassword(const char *password); 25 | std::string getUsername(); 26 | 27 | void addSample(ChannelList channels, double time, double v0); 28 | void addSample(ChannelList channels, double time, double v0, double v1); 29 | void addSample(ChannelList channels, double time, double v0, double v1, double v2); 30 | void addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3); 31 | void addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3, double v4); 32 | void addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3, double v4, double v5); 33 | void addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3, double v4, double v5, double v6); 34 | void addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3, double v4, double v5, double v6, double v7); 35 | void addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3, double v4, double v5, double v6, double v7, double v8); 36 | void addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3, double v4, double v5, double v6, double v7, double v8, double v9); 37 | void addSample(ChannelList channels, double time, double *channelValues, unsigned int channelCount); 38 | 39 | 40 | void setServerBaseURL(const char *serverBaseURL); 41 | std::string getServerBaseURL(); 42 | 43 | void setMaximumAge(double seconds); 44 | double getMaximumAge(); 45 | 46 | void setMaximumUploadSampleCount(unsigned int sampleCount); 47 | unsigned int getMaximumUploadSampleCount(); 48 | 49 | static double now(); 50 | 51 | enum { 52 | DEFAULT_MAXIMUM_AGE = 10, 53 | DEFAULT_MAXIMUM_UPLOAD_SAMPLE_COUNT = 1000 54 | }; 55 | static const char *DEFAULT_SERVER_BASE_URL; 56 | 57 | private: 58 | Mutex mutex; 59 | std::vector samples; 60 | std::string username; 61 | std::string password; 62 | std::string serverBaseURL; // e.g. http://flxtest.bodytrack.org 63 | double maximumAge; // in seconds 64 | unsigned int maximumUploadSampleCount; 65 | 66 | Samples *getSamples(ChannelList channels); 67 | 68 | Mutex uploadScheduleMutex; 69 | bool uploadScheduled; 70 | void scheduleUpload(); 71 | void uploadNow(); 72 | }; 73 | 74 | 75 | 76 | #endif /* defined(__Stetho__FluxtreamUploader__) */ 77 | -------------------------------------------------------------------------------- /Shared/FluxtreamUploader.mm: -------------------------------------------------------------------------------- 1 | // 2 | // FluxtreamUploader.cpp 3 | // Stetho 4 | // 5 | // Created by rsargent on 12/19/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #include 10 | 11 | #import 12 | 13 | #include "Mutex.h" 14 | #include "Samples.h" 15 | #include "Utils.h" 16 | 17 | #include "FluxtreamUploader.h" 18 | 19 | const char *FluxtreamUploader::DEFAULT_SERVER_BASE_URL = "http://flxtest.bodytrack.org"; 20 | 21 | FluxtreamUploader::FluxtreamUploader() : 22 | serverBaseURL(DEFAULT_SERVER_BASE_URL), 23 | maximumAge(DEFAULT_MAXIMUM_AGE), 24 | maximumUploadSampleCount(DEFAULT_MAXIMUM_UPLOAD_SAMPLE_COUNT), 25 | uploadScheduled(false) { 26 | } 27 | 28 | void FluxtreamUploader::setUsername(const char *newUsername) { 29 | ScopedLock l(mutex); 30 | username = newUsername; 31 | } 32 | 33 | std::string FluxtreamUploader::getUsername() { 34 | return username; 35 | } 36 | 37 | void FluxtreamUploader::setPassword(const char *newPassword) { 38 | ScopedLock l(mutex); 39 | password = newPassword; 40 | } 41 | 42 | void FluxtreamUploader::addSample(ChannelList channels, double time, double v0) { 43 | double values[] = {v0}; 44 | addSample(channels, time, values, sizeof(values)/sizeof(values[0])); 45 | } 46 | 47 | void FluxtreamUploader::addSample(ChannelList channels, double time, double v0, double v1) { 48 | scheduleUpload(); 49 | double values[] = {v0, v1}; 50 | addSample(channels, time, values, sizeof(values)/sizeof(values[0])); 51 | } 52 | 53 | void FluxtreamUploader::addSample(ChannelList channels, double time, double v0, double v1, double v2) { 54 | double values[] = {v0, v1, v2}; 55 | addSample(channels, time, values, sizeof(values)/sizeof(values[0])); 56 | } 57 | 58 | void FluxtreamUploader::addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3) { 59 | double values[] = {v0, v1, v2, v3}; 60 | addSample(channels, time, values, sizeof(values)/sizeof(values[0])); 61 | } 62 | 63 | void FluxtreamUploader::addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3, double v4) { 64 | double values[] = {v0, v1, v2, v3, v4}; 65 | addSample(channels, time, values, sizeof(values)/sizeof(values[0])); 66 | } 67 | 68 | void FluxtreamUploader::addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3, double v4, double v5) { 69 | double values[] = {v0, v1, v2, v3, v4, v5}; 70 | addSample(channels, time, values, sizeof(values)/sizeof(values[0])); 71 | } 72 | 73 | void FluxtreamUploader::addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3, double v4, double v5, double v6) { 74 | double values[] = {v0, v1, v2, v3, v4, v5, v6}; 75 | addSample(channels, time, values, sizeof(values)/sizeof(values[0])); 76 | } 77 | 78 | void FluxtreamUploader::addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3, double v4, double v5, double v6, double v7) { 79 | double values[] = {v0, v1, v2, v3, v4, v5, v6, v7}; 80 | addSample(channels, time, values, sizeof(values)/sizeof(values[0])); 81 | } 82 | 83 | void FluxtreamUploader::addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3, double v4, double v5, double v6, double v7, double v8) { 84 | double values[] = {v0, v1, v2, v3, v4, v5, v6, v7, v8}; 85 | addSample(channels, time, values, sizeof(values)/sizeof(values[0])); 86 | } 87 | 88 | void FluxtreamUploader::addSample(ChannelList channels, double time, double v0, double v1, double v2, double v3, double v4, double v5, double v6, double v7, double v8, double v9) { 89 | double values[] = {v0, v1, v2, v3, v4, v5, v6, v7, v8, v9}; 90 | addSample(channels, time, values, sizeof(values)/sizeof(values[0])); 91 | } 92 | 93 | void FluxtreamUploader::addSample(ChannelList channels, double time, double *channelValues, unsigned int channelCount) { 94 | { 95 | ScopedLock l(mutex); 96 | getSamples(channels)->addSample(time, channelValues, channelCount); 97 | } 98 | scheduleUpload(); 99 | } 100 | 101 | void FluxtreamUploader::setServerBaseURL(const char *newServerBaseURL) { 102 | serverBaseURL = newServerBaseURL; 103 | } 104 | 105 | std::string FluxtreamUploader::getServerBaseURL() { 106 | return serverBaseURL; 107 | } 108 | 109 | void FluxtreamUploader::setMaximumAge(double seconds) { 110 | maximumAge = seconds; 111 | } 112 | 113 | double FluxtreamUploader::getMaximumAge() { 114 | return maximumAge; 115 | } 116 | 117 | void FluxtreamUploader::setMaximumUploadSampleCount(unsigned int sampleCount) { 118 | maximumUploadSampleCount = sampleCount; 119 | } 120 | 121 | unsigned int FluxtreamUploader::getMaximumUploadSampleCount() { 122 | return maximumUploadSampleCount; 123 | } 124 | 125 | double FluxtreamUploader::now() { 126 | return doubletime(); 127 | } 128 | 129 | /////////////// Private 130 | 131 | // Not thread-safe; caller must hold lock on "mutex" 132 | Samples *FluxtreamUploader::getSamples(ChannelList channels) { 133 | int idx = channels.getIndex(); 134 | if (idx >= samples.size()) { 135 | samples.resize(idx + 1); 136 | } 137 | if (samples[idx] == NULL) { 138 | samples[idx] = new Samples(); 139 | for (unsigned i = 0; i < channels.size(); i++) { 140 | samples[idx]->addChannel(channels.get(i)); 141 | } 142 | } 143 | return samples[idx]; 144 | } 145 | 146 | void FluxtreamUploader::uploadNow() { 147 | { 148 | ScopedLock l(&uploadScheduleMutex); 149 | uploadScheduled = false; 150 | } 151 | 152 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; 153 | [request setHTTPMethod:@"POST"]; 154 | [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; 155 | 156 | NSString *deviceNickname = @"PolarStrap"; 157 | NSString *urlString = [NSString 158 | stringWithFormat:@"%@/api/bodytrack/jupload?dev_nickname=%@", 159 | self.serverPrefix, deviceNickname]; 160 | NSURL *url = [NSURL URLWithString:urlString]; 161 | [request setURL:url]; 162 | 163 | size_t uploadCount = std::min(samples.size(), _maximumUploadSampleCount); 164 | size_t nextSequence; // set by getJSON 165 | std::string json = samples.getJSON(uploadCount, nextSequence); 166 | 167 | NSData *body = [NSData dataWithBytes: json.c_str() length: json.length()]; 168 | NSString *bodyLength = [NSString stringWithFormat:@"%ld", (long)[body length]]; 169 | [request setValue:bodyLength forHTTPHeaderField:@"Content-Length"]; 170 | 171 | NSString *authStr = [NSString stringWithFormat:@"%@:%@", self.username, self.password]; 172 | 173 | NSData *authData = [authStr dataUsingEncoding:NSUTF8StringEncoding]; 174 | NSString *authValue = [NSString stringWithFormat:@"Basic %@", base64encode(authData)]; 175 | [request setValue:authValue forHTTPHeaderField:@"Authorization"]; 176 | 177 | [request setHTTPBody:body]; 178 | 179 | NSLog(@"about to post %ld samples", uploadCount); 180 | 181 | lastResult = @"Connecting to server for upload..."; 182 | 183 | // Delete cookies to force authentication from scratch each time 184 | NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 185 | NSArray *cookies = [cookieStorage cookies]; 186 | for (NSHTTPCookie *cookie in cookies) { 187 | [cookieStorage deleteCookie:cookie]; 188 | } 189 | 190 | // to get HTTP status on error, consider something like 191 | // initWithRequest:delegate: and didReceiveResponse 192 | [NSURLConnection sendAsynchronousRequest:request 193 | queue:[NSOperationQueue mainQueue] 194 | completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { 195 | NSLog(@"got %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); 196 | long errorCode = [error code]; 197 | long statusCode = [(NSHTTPURLResponse*) response statusCode]; 198 | NSLog(@"error code %ld", errorCode); 199 | NSLog(@"status code %ld", statusCode); 200 | NSString *text = [[NSString alloc] initWithData: data encoding:NSUTF8StringEncoding]; 201 | if (errorCode == 0) { 202 | lastResult = @""; 203 | [[NSNotificationCenter defaultCenter] postNotificationName:BT_NOTIFICATION_UPLOAD_SUCCEEDED object:self]; 204 | if (uploadCount) { 205 | samples.deleteUntilSequence(nextSequence); 206 | lastUploadTime = doubletime(); 207 | } 208 | } else if ([text rangeOfString:@"Bad credentials"].location != NSNotFound) { 209 | [[NSNotificationCenter defaultCenter] postNotificationName:BT_NOTIFICATION_UPLOAD_AUTH_FAILED object:self]; 210 | lastResult = @"Incorrect username or password."; 211 | } else { 212 | [[NSNotificationCenter defaultCenter] postNotificationName:BT_NOTIFICATION_UPLOAD_NETWORK_ERROR object:self]; 213 | lastResult = @"Unable to contact server."; 214 | [self scheduleUpload]; 215 | } 216 | }]; 217 | } 218 | 219 | 220 | 221 | } 222 | 223 | void FluxtreamUploader::scheduleUpload() { 224 | ScopedLock l(uploadScheduleMutex); 225 | if (uploadScheduled) return; 226 | uploadScheduled = true; 227 | [[NSOperationQueue mainQueue] addOperationWithBlock: ^{ 228 | [NSTimer scheduledTimerWithTimeInterval:maximumAge 229 | target:[NSBlockOperation blockOperationWithBlock:^{ 230 | uploadNow(); 231 | }] 232 | selector:@selector(main) userInfo:nil repeats:NO 233 | ]; 234 | }]; 235 | } 236 | 237 | /* 238 | Start timer in main thread: use addOperationWithBlock 239 | 240 | Timer fires in main thread: use 241 | 242 | NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.7 243 | target:[NSBlockOperation blockOperationWithBlock:^{ }] 244 | selector:@selector(main) 245 | userInfo:nil 246 | repeats:NO 247 | ]; 248 | 249 | 250 | */ 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /Shared/FluxtreamUploaderObjc.h: -------------------------------------------------------------------------------- 1 | // 2 | // FluxtreamUploaderObjc.h 3 | // HeartRateMonitor 4 | // 5 | // Created by bodytrack on 10/13/12. 6 | // 7 | 8 | #import 9 | 10 | #define BT_NOTIFICATION_UPLOAD_AUTH_FAILED @"bt_upload_auth_failed" 11 | #define BT_NOTIFICATION_UPLOAD_NETWORK_ERROR @"bt_upload_network_error" 12 | #define BT_NOTIFICATION_UPLOAD_SUCCEEDED @"bt_upload_succeeded" 13 | 14 | @interface FluxtreamUploaderObjc : NSObject 15 | { 16 | void *samplesPtr; 17 | NSTimer *uploadTimer; 18 | double lastUploadTime; 19 | NSString *lastResult; 20 | unsigned char uploadScheduled; 21 | } 22 | 23 | - (void) addChannel:(NSString*)name; // adds numeric channel 24 | - (void) addStringChannel:(NSString*)name; 25 | 26 | - (void) addSample:(double)time ch0:(double)ch0; 27 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1; 28 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2; 29 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3; 30 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3 ch4:(double)ch4; 31 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3 ch4:(double)ch4 ch5:(double)ch5; 32 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3 ch4:(double)ch4 ch5:(double)ch5 ch6:(double)ch6; 33 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3 ch4:(double)ch4 ch5:(double)ch5 ch6:(double)ch6 ch7:(double)ch7; 34 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3 ch4:(double)ch4 ch5:(double)ch5 ch6:(double)ch6 ch7:(double)ch7 ch8:(double)ch8; 35 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3 ch4:(double)ch4 ch5:(double)ch5 ch6:(double)ch6 ch7:(double)ch7 ch8:(double)ch8 ch9:(double)ch9; 36 | 37 | - (void) addSample:(double)time values:(double[])values count:(unsigned int)count; 38 | - (void) addSample:(double)time numericValues:(double[])numericValues numericCount:(unsigned int)numericCount stringValues:(NSArray *)stringValues; 39 | 40 | 41 | - (void) uploadNow; 42 | - (size_t) sampleCount; 43 | // Returns -1 if never uploaded 44 | - (double) timeSinceLastUpload; 45 | - (NSString*) getStatus; 46 | + (double) now; 47 | 48 | @property (strong) NSString *deviceNickname; 49 | @property (strong) NSString *username; 50 | @property (strong) NSString *password; 51 | @property (strong) NSString *serverPrefix; 52 | @property double maximumAge; 53 | @property size_t maximumUploadSampleCount; 54 | @property BOOL logSamples; 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /Shared/FluxtreamUploaderObjc.mm: -------------------------------------------------------------------------------- 1 | // 2 | // FluxtreamUploaderObjc.mm 3 | // HeartRateMonitor 4 | // 5 | // Created by bodytrack on 10/13/12. 6 | // 7 | 8 | #import "FluxtreamUploaderObjc.h" 9 | #include "NSUtils.h" 10 | #include "Utils.h" 11 | #include "Samples.h" 12 | #import "Constants.h" 13 | 14 | #include 15 | #include 16 | 17 | @implementation FluxtreamUploaderObjc 18 | 19 | - (id)init 20 | { 21 | if (self = [super init]) { 22 | samplesPtr = new Samples(); 23 | uploadTimer = nil; 24 | self.maximumAge = 60.0; 25 | self.maximumUploadSampleCount = 10000; 26 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 27 | self.serverPrefix = [defaults objectForKey:DEFAULTS_SERVER]; 28 | lastUploadTime = 0; 29 | uploadScheduled = 0; 30 | lastResult = @""; 31 | self.logSamples = NO; 32 | } 33 | return self; 34 | } 35 | 36 | -(void)dealloc { 37 | delete (Samples*)samplesPtr; 38 | samplesPtr = NULL; 39 | } 40 | 41 | - (Samples*)samples 42 | { 43 | return (Samples*)samplesPtr; 44 | } 45 | 46 | - (void)uploadNow 47 | { 48 | OSAtomicTestAndClear(7, &uploadScheduled); 49 | uploadTimer = nil; 50 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; 51 | [request setHTTPMethod:@"POST"]; 52 | [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; 53 | 54 | NSString *urlString = [NSString 55 | stringWithFormat:@"http://%@/api/bodytrack/jupload?dev_nickname=%@", 56 | self.serverPrefix, self.deviceNickname]; 57 | NSURL *url = [NSURL URLWithString:urlString]; 58 | [request setURL:url]; 59 | 60 | size_t uploadCount = std::min([self samples]->size(), _maximumUploadSampleCount); 61 | size_t nextSequence; // set by getJSON 62 | std::string json = [self samples]->getJSON(uploadCount, nextSequence); 63 | 64 | NSData *body = [NSData dataWithBytes: json.c_str() length: json.length()]; 65 | NSString *bodyLength = [NSString stringWithFormat:@"%ld", (long)[body length]]; 66 | [request setValue:bodyLength forHTTPHeaderField:@"Content-Length"]; 67 | 68 | NSString *authStr = [NSString stringWithFormat:@"%@:%@", self.username, self.password]; 69 | 70 | NSData *authData = [authStr dataUsingEncoding:NSUTF8StringEncoding]; 71 | NSString *authValue = [NSString stringWithFormat:@"Basic %@", base64encode(authData)]; 72 | [request setValue:authValue forHTTPHeaderField:@"Authorization"]; 73 | 74 | [request setHTTPBody:body]; 75 | 76 | NSLog(@"%@ about to post %ld samples", self.deviceNickname, uploadCount); 77 | 78 | lastResult = @"Connecting to server for upload..."; 79 | 80 | // Delete cookies to force authentication from scratch each time 81 | NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 82 | NSArray *cookies = [cookieStorage cookies]; 83 | for (NSHTTPCookie *cookie in cookies) { 84 | [cookieStorage deleteCookie:cookie]; 85 | } 86 | 87 | // to get HTTP status on error, consider something like 88 | // initWithRequest:delegate: and didReceiveResponse 89 | [NSURLConnection sendAsynchronousRequest:request 90 | queue:[NSOperationQueue mainQueue] 91 | completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { 92 | NSLog(@"%@ got %@", self.deviceNickname, [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); 93 | if (error) { 94 | NSLog(@"%@ got error code %ld", self.deviceNickname, (long)[error code]); 95 | switch ([error code]) { 96 | case NSURLErrorUserCancelledAuthentication: 97 | case NSURLErrorUserAuthenticationRequired: 98 | NSLog(@"Authentication error"); 99 | [[NSNotificationCenter defaultCenter] postNotificationName:BT_NOTIFICATION_UPLOAD_AUTH_FAILED object:self]; 100 | lastResult = @"%Incorrect username or password."; 101 | break; 102 | default: 103 | lastResult = @"Unable to contact server."; 104 | [[NSNotificationCenter defaultCenter] postNotificationName:BT_NOTIFICATION_UPLOAD_NETWORK_ERROR object:self]; 105 | [self scheduleUpload]; 106 | break; 107 | } 108 | } else { 109 | int statusCode = (int)[(NSHTTPURLResponse*) response statusCode]; 110 | NSLog(@"%@ success with HTTP status %d", self.deviceNickname, statusCode); 111 | lastResult = @""; 112 | [[NSNotificationCenter defaultCenter] postNotificationName:BT_NOTIFICATION_UPLOAD_SUCCEEDED object:self]; 113 | if (uploadCount) { 114 | [self samples]->deleteUntilSequence(nextSequence); 115 | lastUploadTime = doubletime(); 116 | } 117 | } 118 | }]; 119 | } 120 | 121 | - (double) timeSinceLastUpload 122 | { 123 | if (lastUploadTime == 0) return -1; 124 | return doubletime() - lastUploadTime; 125 | } 126 | 127 | - (NSString*) getStatus 128 | { 129 | double age = [self timeSinceLastUpload]; 130 | NSString *status; 131 | if (age < 0) { 132 | status = [NSString stringWithFormat: @"%@\nNo data uploaded.", lastResult]; 133 | } else { 134 | status = [NSString stringWithFormat: @"%@\nLast data uploaded %@ ago.", lastResult, printDuration(age)]; 135 | } 136 | return status; 137 | } 138 | 139 | - (void) clearStatus 140 | { 141 | lastResult = @""; 142 | } 143 | 144 | - (void) addChannel: (NSString*)name 145 | { 146 | [self samples]->addChannel([name UTF8String]); 147 | } 148 | 149 | - (void) addStringChannel: (NSString*)name 150 | { 151 | [self samples]->addStringChannel([name UTF8String]); 152 | } 153 | 154 | - (void) addSample:(double)time ch0:(double)ch0 155 | { 156 | double values[] = {ch0}; 157 | [self addSample:time values:values count:sizeof(values)/sizeof(values[0])]; 158 | } 159 | 160 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 161 | { 162 | double values[] = {ch0, ch1}; 163 | [self addSample:time values:values count:sizeof(values)/sizeof(values[0])]; 164 | } 165 | 166 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 167 | { 168 | double values[] = {ch0, ch1, ch2}; 169 | [self addSample:time values:values count:sizeof(values)/sizeof(values[0])]; 170 | } 171 | 172 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3 173 | { 174 | double values[] = {ch0, ch1, ch2, ch3}; 175 | [self addSample:time values:values count:sizeof(values)/sizeof(values[0])]; 176 | } 177 | 178 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3 ch4:(double)ch4 179 | { 180 | double values[] = {ch0, ch1, ch2, ch3, ch4}; 181 | [self addSample:time values:values count:sizeof(values)/sizeof(values[0])]; 182 | } 183 | 184 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3 ch4:(double)ch4 ch5:(double)ch5 185 | { 186 | double values[] = {ch0, ch1, ch2, ch3, ch4, ch5}; 187 | [self addSample:time values:values count:sizeof(values)/sizeof(values[0])]; 188 | } 189 | 190 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3 ch4:(double)ch4 ch5:(double)ch5 ch6:(double)ch6 191 | { 192 | double values[] = {ch0, ch1, ch2, ch3, ch4, ch5, ch6}; 193 | [self addSample:time values:values count:sizeof(values)/sizeof(values[0])]; 194 | } 195 | 196 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3 ch4:(double)ch4 ch5:(double)ch5 ch6:(double)ch6 ch7:(double)ch7 197 | { 198 | double values[] = {ch0, ch1, ch2, ch3, ch4, ch5, ch6, ch7}; 199 | [self addSample:time values:values count:sizeof(values)/sizeof(values[0])]; 200 | } 201 | 202 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3 ch4:(double)ch4 ch5:(double)ch5 ch6:(double)ch6 ch7:(double)ch7 ch8:(double)ch8 203 | { 204 | double values[] = {ch0, ch1, ch2, ch3, ch4, ch5, ch6, ch7, ch8}; 205 | [self addSample:time values:values count:sizeof(values)/sizeof(values[0])]; 206 | } 207 | 208 | - (void) addSample:(double)time ch0:(double)ch0 ch1:(double)ch1 ch2:(double)ch2 ch3:(double)ch3 ch4:(double)ch4 ch5:(double)ch5 ch6:(double)ch6 ch7:(double)ch7 ch8:(double)ch8 ch9:(double)ch9 209 | { 210 | double values[] = {ch0, ch1, ch2, ch3, ch4, ch5, ch6, ch7, ch8, ch9}; 211 | [self addSample:time values:values count:sizeof(values)/sizeof(values[0])]; 212 | } 213 | 214 | - (void) addSample:(double)time values:(double*)values count:(unsigned int)count 215 | { 216 | [self addSample:time numericValues:values numericCount:count stringValues:nil]; 217 | } 218 | 219 | - (void) addSample:(double)time numericValues:(double[]) numericValues numericCount:(unsigned int)numericCount stringValues:(NSArray *)stringValues 220 | { 221 | size_t sequence; 222 | if (stringValues == nil) { 223 | sequence = [self samples]->addSample(time, numericValues, numericCount); 224 | } else { 225 | std::vector stdStringValues([stringValues count]); 226 | for (unsigned i = 0; i < stdStringValues.size(); i++) { 227 | stdStringValues[i] = [stringValues[i] UTF8String]; 228 | } 229 | sequence = [self samples]->addSample(time, numericValues, numericCount, &stdStringValues[0], (unsigned int)stdStringValues.size()); 230 | } 231 | if (self.logSamples) { 232 | NSLog(@"%s", [self samples]->getSampleJSON(sequence).c_str()); 233 | } 234 | [self scheduleUpload]; 235 | } 236 | 237 | - (void) scheduleUpload 238 | { 239 | if (!OSAtomicTestAndSet(7, &uploadScheduled)) { 240 | [[NSOperationQueue mainQueue] addOperationWithBlock: ^{ 241 | NSLog(@"%@ scheduling upload in %f seconds", self.deviceNickname, self.maximumAge); 242 | uploadTimer = [NSTimer scheduledTimerWithTimeInterval:self.maximumAge target:self selector:@selector(uploadNow) userInfo:nil repeats:NO]; 243 | }]; 244 | } 245 | } 246 | 247 | - (size_t) sampleCount 248 | { 249 | return [self samples]->size(); 250 | } 251 | 252 | + (double)now 253 | { 254 | return doubletime(); 255 | } 256 | 257 | @end 258 | -------------------------------------------------------------------------------- /Shared/Logger.h: -------------------------------------------------------------------------------- 1 | // 2 | // logger.h 3 | // Stetho 4 | // 5 | // Created by rsargent on 10/27/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #include 10 | 11 | #import 12 | 13 | typedef enum { 14 | kLogNormal = 30, 15 | kLogVerbose = 40 16 | } LogVerbosity; 17 | 18 | @interface Logger : NSObject 19 | -(void) log:format, ...; 20 | -(void) logVerbose:format, ...; 21 | -(void) logWithVerbosity:(LogVerbosity)verbosity format:(NSString*)format args:(va_list)args; 22 | -(void) logWithVerbosity:(LogVerbosity)verbosity msg:(NSString*)msg; 23 | @property BOOL debugLog; 24 | @end 25 | 26 | -------------------------------------------------------------------------------- /Shared/Logger.mm: -------------------------------------------------------------------------------- 1 | // System 2 | #include 3 | 4 | // Self 5 | #import "Logger.h" 6 | 7 | @implementation Logger 8 | 9 | -(Logger*) init 10 | { 11 | if (self = [super init]) { 12 | self.debugLog = YES; 13 | } 14 | return self; 15 | } 16 | 17 | -(void) log:format, ... 18 | { 19 | va_list args; 20 | va_start(args, format); 21 | [self logWithVerbosity: kLogNormal format: format args: args]; 22 | va_end(args); 23 | } 24 | 25 | -(void) logVerbose:format, ... 26 | { 27 | va_list args; 28 | va_start(args, format); 29 | [self logWithVerbosity: kLogVerbose format: format args: args]; 30 | va_end(args); 31 | } 32 | 33 | -(void) logWithVerbosity:(LogVerbosity)verbosity format:(NSString*)format args:(va_list)args 34 | { 35 | [self logWithVerbosity:verbosity msg:[[NSString alloc] initWithFormat:format arguments:args]]; 36 | } 37 | 38 | -(void) logWithVerbosity:(LogVerbosity)verbosity msg:(NSString*)msg 39 | { 40 | if (self.debugLog) NSLog(@"%@", msg); 41 | } 42 | 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /Shared/Mutex.h: -------------------------------------------------------------------------------- 1 | // 2 | // Mutex.h 3 | // Stetho 4 | // 5 | // Created by rsargent on 12/19/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #ifndef __Stetho__Mutex__ 10 | #define __Stetho__Mutex__ 11 | 12 | #include 13 | 14 | class Mutex { 15 | private: 16 | pthread_mutex_t mutex; 17 | public: 18 | Mutex() { 19 | mutex = PTHREAD_MUTEX_INITIALIZER; 20 | } 21 | void lock() { 22 | pthread_mutex_lock(&mutex); 23 | } 24 | void unlock() { 25 | pthread_mutex_unlock(&mutex); 26 | } 27 | }; 28 | 29 | class ScopedLock { 30 | 31 | private: 32 | Mutex &mutex; 33 | 34 | public: 35 | ScopedLock(Mutex &m) : mutex(m) { 36 | mutex.lock(); 37 | } 38 | 39 | ~ScopedLock() { 40 | mutex.unlock(); 41 | } 42 | }; 43 | 44 | #endif /* defined(__Stetho__ScopedMutex__) */ 45 | -------------------------------------------------------------------------------- /Shared/NSUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSUtils.h 3 | // HeartRateMonitor 4 | // 5 | // Created by bodytrack on 10/15/12. 6 | // 7 | 8 | #ifndef HeartRateMonitor_NSUtils_h 9 | #define HeartRateMonitor_NSUtils_h 10 | 11 | #include 12 | 13 | NSString *base64encode(NSData *data); 14 | NSString *printDuration(double duration); 15 | NSString *utf8(NSData *data); 16 | NSString *utf8(const std::string &str); 17 | #endif 18 | -------------------------------------------------------------------------------- /Shared/NSUtils.mm: -------------------------------------------------------------------------------- 1 | #include "NSUtils.h" 2 | 3 | NSString *base64encode(NSData *data) { 4 | const char *base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 5 | 6 | const unsigned char *src = (const unsigned char*) [data bytes]; 7 | 8 | NSMutableData *target = 9 | [NSMutableData dataWithCapacity: ([data length] + 2) / 3 * 4]; 10 | 11 | for (long srclength = [data length]; srclength > 0; srclength -= 3) { 12 | unsigned int in = *src++ << 16; 13 | if (srclength > 1) in += *src++ << 8; 14 | if (srclength > 2) in += *src++; 15 | 16 | [target appendBytes:&base64[0x3f & (in >> 18)] length:1]; 17 | [target appendBytes:&base64[0x3f & (in >> 12)] length:1]; 18 | [target appendBytes:srclength > 1 ? &base64[0x3f & (in >> 6)] : "=" length:1]; 19 | [target appendBytes:srclength > 2 ? &base64[0x3f & (in >> 0)] : "=" length:1]; 20 | } 21 | 22 | return [[NSString alloc] initWithData:target encoding:NSASCIIStringEncoding]; 23 | } 24 | 25 | NSString *printDuration(double duration) { 26 | if (duration > 7200) { 27 | return [NSString stringWithFormat: @"%.1f hours", duration / 3600]; 28 | } else if (duration > 120) { 29 | return [NSString stringWithFormat: @"%.1f minutes", duration / 60]; 30 | } else { 31 | return [NSString stringWithFormat: @"%.0f seconds", duration]; 32 | } 33 | } 34 | 35 | NSString *utf8(NSData *data) { 36 | return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 37 | } 38 | 39 | NSString *utf8(const std::string &str) { 40 | return [NSString stringWithUTF8String:str.c_str()]; 41 | } 42 | -------------------------------------------------------------------------------- /Shared/Nickname.h: -------------------------------------------------------------------------------- 1 | // 2 | // Nickname.h 3 | // Stetho 4 | // 5 | // Created by rsargent on 10/25/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #ifndef Stetho_Nickname_h 10 | #define Stetho_Nickname_h 11 | 12 | #include 13 | #include 14 | 15 | std::string computeNickname(const void *data, size_t len); 16 | std::string computeNickname(std::string data); 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /Shared/RawZeoReader.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // RawZeoReader.cpp 3 | // ZeoRelay 4 | // 5 | // Created by rsargent on 10/20/12. 6 | // Copyright (c) 2012 rsargent. All rights reserved. 7 | // 8 | 9 | #include "RawZeoReader.h" 10 | #include "Utils.h" 11 | #include 12 | #include 13 | #include 14 | 15 | RawZeoReader::RawZeoReader() : serialFd(-1) { 16 | } 17 | 18 | bool RawZeoReader::open(const std::string &deviceFilename) { 19 | close(); 20 | serialFd = ::open(deviceFilename.c_str(), O_RDWR | O_NONBLOCK | O_NOCTTY); 21 | if (serialFd == -1) return false; 22 | 23 | struct termios options; 24 | memset(&options, 0, sizeof(options)); 25 | tcgetattr(serialFd, &options); 26 | cfsetispeed(&options, B38400); 27 | cfsetospeed(&options, B38400); 28 | options.c_iflag = 0; 29 | options.c_oflag = 0; 30 | options.c_cflag = CS8 | CREAD | CLOCAL; 31 | options.c_cc[VMIN] = 0; // No min chars to read 32 | options.c_cc[VTIME] = 0; // Don't wait 33 | tcsetattr(serialFd, TCSANOW, &options); 34 | 35 | return true; 36 | } 37 | 38 | bool RawZeoReader::testConnection() { 39 | ZeoPacket packet; 40 | double timeout = 5.0; // seconds 41 | // The Zeo is continually spitting out packets; consider a connection good 42 | // if the zeo sends a packet in the next 5 seconds 43 | return readPacketWithTimeout(packet, timeout); 44 | } 45 | 46 | bool RawZeoReader::autoOpen() { 47 | std::vector serialPorts = allSerialPorts(); 48 | for (unsigned i = 0; i < serialPorts.size(); i++) { 49 | if (open(serialPorts[i]) && testConnection()) { 50 | return true; 51 | } 52 | close(); 53 | } 54 | return false; 55 | } 56 | 57 | void RawZeoReader::close() { 58 | if (serialFd != -1) { 59 | ::close(serialFd); 60 | serialFd = -1; 61 | } 62 | } 63 | 64 | bool RawZeoReader::readPacketWithTimeout(ZeoPacket &packet, double timeout) { 65 | double deadline = timeout + doubletime(); 66 | 67 | while (doubletime() < deadline) { 68 | if (read8(deadline) != 'A') continue; 69 | if (read8(deadline) != '4') continue; 70 | int checksum = read8(deadline); 71 | int length = read16(deadline); 72 | if (read16(deadline) != (0xffff ^ length)) continue; 73 | if (length == 0) { 74 | fprintf(stderr, "readPacketWithTimeout: length=0, rejecting"); 75 | continue; 76 | } 77 | 78 | int timeSecs = read8(deadline); 79 | int timeFrac = read16(deadline); 80 | packet.time = timeSecs + timeFrac / 65536.0; 81 | 82 | packet.sequence = read8(deadline); 83 | packet.data.resize(length); 84 | 85 | read(packet.data, deadline); 86 | unsigned char computedChecksum = 0; 87 | for (unsigned i = 0; i < packet.data.size(); i++) { 88 | computedChecksum += packet.data[i]; 89 | } 90 | if (checksum != computedChecksum) { 91 | fprintf(stderr, "Checksum error: 0x%x != 0x%x", checksum, computedChecksum); 92 | continue; 93 | } 94 | if (doubletime() >= deadline) break; 95 | fprintf(stderr, "readPacketWithTimeout(%g) returns type 0x%02x, data length %ld", 96 | timeout, packet.data[1], (long) packet.data.size()); 97 | return true; 98 | } 99 | fprintf(stderr, "readPacketWithTimeout(%g): timeout", timeout); 100 | return false; 101 | } 102 | 103 | 104 | RawZeoReader::~RawZeoReader() { 105 | close(); 106 | } 107 | 108 | std::vector RawZeoReader::allSerialPorts() { 109 | return glob("/dev/tty.*"); 110 | } 111 | 112 | int RawZeoReader::read8(double deadline) { 113 | unsigned char c; 114 | if (!waitForRead(deadline)) return -1; 115 | if (1 != ::read(serialFd, &c, 1)) return -1; 116 | return c; 117 | } 118 | 119 | int RawZeoReader::read16(double deadline) { 120 | int low = read8(deadline); 121 | int high = read8(deadline); 122 | if (low == -1 || high == -1) return -1; 123 | return (high << 8) | low; 124 | } 125 | 126 | bool RawZeoReader::read(std::vector data, double deadline) { 127 | size_t nread = 0; 128 | while (nread < data.size()) { 129 | if (!waitForRead(deadline)) return false; 130 | long ret = ::read(serialFd, &data[nread], data.size() - nread); 131 | if (ret < 0) return false; 132 | nread += ret; 133 | } 134 | return true; 135 | } -------------------------------------------------------------------------------- /Shared/RawZeoReader.h: -------------------------------------------------------------------------------- 1 | // 2 | // RawZeoReader.h 3 | // ZeoRelay 4 | // 5 | // Created by rsargent on 10/20/12. 6 | // Copyright (c) 2012 rsargent. All rights reserved. 7 | // 8 | 9 | #ifndef __ZeoRelay__RawZeoReader__ 10 | #define __ZeoRelay__RawZeoReader__ 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | struct ZeoPacket { 17 | double time; 18 | int sequence; 19 | std::vector data; 20 | }; 21 | 22 | class RawZeoReader { 23 | private: 24 | int serialFd; 25 | public: 26 | RawZeoReader(); 27 | bool open(const std::string &deviceFilename); 28 | bool testConnection(); 29 | bool autoOpen(); 30 | void close(); 31 | bool readPacketWithTimeout(ZeoPacket &packet, double timeout); 32 | ~RawZeoReader(); 33 | private: 34 | static std::vector allSerialPorts(); 35 | bool waitForRead(double deadline); 36 | int read8(double deadline); 37 | int read16(double deadline); 38 | bool read(std::vector data, double deadline); 39 | }; 40 | 41 | #endif /* defined(__ZeoRelay__RawZeoReader__) */ 42 | -------------------------------------------------------------------------------- /Shared/RawZeoRelay.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // RawZeoRelay.cpp 3 | // ZeoRelay 4 | // 5 | // Created by rsargent on 10/23/12. 6 | // Copyright (c) 2012 rsargent. All rights reserved. 7 | // 8 | 9 | #include "RawZeoRelay.h" 10 | -------------------------------------------------------------------------------- /Shared/RawZeoRelay.h: -------------------------------------------------------------------------------- 1 | // 2 | // RawZeoRelay.h 3 | // ZeoRelay 4 | // 5 | // Created by rsargent on 10/23/12. 6 | // Copyright (c) 2012 rsargent. All rights reserved. 7 | // 8 | 9 | #ifndef __ZeoRelay__RawZeoRelay__ 10 | #define __ZeoRelay__RawZeoRelay__ 11 | 12 | #include 13 | #include "RawZeoReader.h" 14 | #include "FluxtreamUploader.h" 15 | 16 | class RawZeoRelay { 17 | public: 18 | RawZeoReader reader; 19 | FluxtreamUploader uploader; 20 | 21 | RawZeoRelay(); 22 | 23 | // Relay until Zeo stops sending 24 | // Assumes reader is already connected 25 | void relay() { 26 | double timeout = 5.0; // seconds 27 | ZeoPacket packet; 28 | fprintf(stderr, "starting relay\n"); 29 | while (reader.readPacketWithTimeout(packet, timeout)) { 30 | fprintf(stderr, "got a packet, yo!\n"); 31 | } 32 | fprintf(stderr, "relay timed out\n"); 33 | } 34 | 35 | 36 | void autoRelay() { 37 | while (1) { 38 | reader.autoOpen(); 39 | relay(); 40 | } 41 | } 42 | } 43 | 44 | #endif /* defined(__ZeoRelay__RawZeoRelay__) */ 45 | -------------------------------------------------------------------------------- /Shared/Samples.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Samples.cpp 3 | // HeartRateMonitor 4 | // 5 | // Created by bodytrack on 10/13/12. 6 | // 7 | 8 | #include 9 | 10 | #include "Utils.h" 11 | #include "Samples.h" 12 | 13 | Samples::Samples() : firstSequence(0) {} 14 | 15 | void Samples::addChannel(const std::string &name) { 16 | ScopedLock l(lock); 17 | assert(numericValues.empty()); 18 | assert(stringValues.empty()); 19 | numericChannelNames.push_back(name); 20 | } 21 | 22 | void Samples::addStringChannel(const std::string &name) { 23 | ScopedLock l(lock); 24 | assert(numericValues.empty()); 25 | assert(stringValues.empty()); 26 | stringChannelNames.push_back(name); 27 | } 28 | 29 | // Returns sequence # of sample added 30 | size_t Samples::addSample(double time, const double *numericVals, unsigned int numericCount, 31 | const std::string *stringVals, unsigned int stringCount) { 32 | ScopedLock l(lock); 33 | assert(numericChannelNames.size() == numericCount); 34 | assert(stringChannelNames.size() == stringCount); 35 | size_t sequence = firstSequence + sampleTimes.size(); 36 | sampleTimes.push_back(time); 37 | numericValues.insert(numericValues.end(), numericVals, numericVals + numericCount); 38 | stringValues.insert(stringValues.end(), stringVals, stringVals + stringCount); 39 | return sequence; 40 | } 41 | 42 | size_t Samples::size() const { 43 | return sampleTimes.size(); 44 | } 45 | 46 | void Samples::deleteUntilSequence(size_t sequence) { 47 | ScopedLock l(lock); 48 | if (sequence > firstSequence) { 49 | size_t samplesToDelete = std::min(sequence - firstSequence, size()); 50 | sampleTimes.erase(sampleTimes.begin(), sampleTimes.begin() + samplesToDelete); 51 | numericValues.erase(numericValues.begin(), numericValues.begin() + samplesToDelete * numericChannelNames.size()); 52 | stringValues.erase(stringValues.begin(), stringValues.begin() + samplesToDelete * stringChannelNames.size()); 53 | firstSequence += samplesToDelete; 54 | } 55 | } 56 | 57 | std::string Samples::getJSON(size_t sampleCount, 58 | size_t &returnNextSequence) const { 59 | ScopedLock l(lock); 60 | sampleCount = std::min(sampleCount, size()); 61 | returnNextSequence = firstSequence + sampleCount; 62 | std::string ret = "{"; 63 | ret += "\"channel_names\": ["; 64 | int col = 0; 65 | for (unsigned i = 0; i < numericChannelNames.size(); i++) { 66 | if (col++) ret += ","; 67 | ret += string_printf("\"%s\"", numericChannelNames[i].c_str()); 68 | } 69 | for (unsigned i = 0; i < stringChannelNames.size(); i++) { 70 | if (col++) ret += ","; 71 | ret += string_printf("\"%s\"", stringChannelNames[i].c_str()); 72 | } 73 | ret += "]"; 74 | ret += ","; 75 | ret += "\"data\":["; 76 | for (unsigned row = 0; row < sampleCount; row++) { 77 | 78 | if (row) ret += ","; 79 | ret += string_printf("[%.3f", sampleTimes[row]); 80 | for (unsigned i = 0; i < numericChannelNames.size(); i++) { 81 | // TODO(rsargent): don't hardcode the precision here 82 | ret += string_printf(",%.3f", numericValues[row * numericChannelNames.size() + i]); 83 | } 84 | for (unsigned i = 0; i < stringChannelNames.size(); i++) { 85 | // TODO(rsargent): quote string JSON-style 86 | ret += ",\"" + stringValues[row * stringChannelNames.size() + i] + "\""; 87 | } 88 | ret += "]"; 89 | } 90 | ret += "]"; 91 | ret += "}"; 92 | return ret; 93 | } 94 | 95 | std::string Samples::getSampleJSON(size_t sequence) const { 96 | ScopedLock l(lock); 97 | int index = (int) (sequence - firstSequence); 98 | std::string ret = "{"; 99 | int col = 0; 100 | // TODO(rsargent): quote field name? 101 | for (unsigned i = 0; i < numericChannelNames.size(); i++) { 102 | if (col++) ret += ","; 103 | ret += string_printf("%s:%g", numericChannelNames[i].c_str(), numericValues[index * numericChannelNames.size() + i]); 104 | } 105 | for (unsigned i = 0; i < stringChannelNames.size(); i++) { 106 | if (col++) ret += ","; 107 | // TODO(rsargent): quote string JSON-style 108 | ret += string_printf("%s:\"%s\"", stringChannelNames[i].c_str(), stringValues[index * stringChannelNames.size() + i].c_str()); 109 | } 110 | ret += "}"; 111 | return ret; 112 | } 113 | -------------------------------------------------------------------------------- /Shared/Samples.h: -------------------------------------------------------------------------------- 1 | // 2 | // Samples.h 3 | // HeartRateMonitor 4 | // 5 | // 6 | 7 | #ifndef __HeartRateMonitor__Samples__ 8 | #define __HeartRateMonitor__Samples__ 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include "Mutex.h" 17 | 18 | class Samples { 19 | private: 20 | std::deque sampleTimes; 21 | std::deque numericValues; 22 | std::deque stringValues; 23 | std::vector numericChannelNames; 24 | std::vector stringChannelNames; 25 | // Sequence number of first sample contained in values. 26 | // Starts as zero and increases as deleteUntilSequence is called 27 | size_t firstSequence; 28 | mutable Mutex lock; 29 | 30 | public: 31 | Samples(); 32 | // Add channel with given name 33 | // All channels must be added before first sample is added 34 | void addChannel(const std::string &name); 35 | void addStringChannel(const std::string &name); 36 | 37 | // Append new sample. Returns sequence # for the sample 38 | size_t addSample(double time, const double *numericValues, unsigned int numericCount, 39 | const std::string *stringValues = NULL, unsigned int stringCount = 0); 40 | 41 | size_t size() const; 42 | size_t columnCount() const; 43 | 44 | // Delete samples such that first sample contained in values is sequence 45 | // If sequence is beyond end of values, delete all samples and make the 46 | // next sequence captured be one plus the last sample deleted 47 | void deleteUntilSequence(size_t sequence); 48 | 49 | // Emit JSON for up to sampleCount samples. Use (size_t)-1 to capture all samples. 50 | // Returns sequence of the first sample beyond that returned by getJSON, for 51 | // future call to deleteUntilSequence 52 | std::string getJSON(size_t sampleCount, size_t &returnNextSequence) const; 53 | 54 | // Emit JSON for single sample 55 | std::string getSampleJSON(size_t sequence) const; 56 | }; 57 | 58 | #endif /* defined(__HeartRateMonitor__Samples__) */ 59 | -------------------------------------------------------------------------------- /Shared/TextViewLogger.h: -------------------------------------------------------------------------------- 1 | // 2 | // TextViewLogger.h 3 | // Stetho 4 | // 5 | // Created by rsargent on 10/27/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import "Logger.h" 10 | 11 | @interface TextViewLogger : Logger 12 | 13 | @property (weak, nonatomic) UITextView *textView; 14 | @property (nonatomic) LogVerbosity maxDisplayedVerbosity; 15 | 16 | -(void) logWithVerbosity:(LogVerbosity)verbosity msg:(NSString*)msg; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Shared/TextViewLogger.mm: -------------------------------------------------------------------------------- 1 | // 2 | // TextViewLogger.m 3 | // Stetho 4 | // 5 | // Created by rsargent on 10/27/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #import "TextViewLogger.h" 10 | #include 11 | 12 | @implementation TextViewLogger 13 | - (TextViewLogger *)init { 14 | self = [super init]; 15 | if (self) { 16 | self.maxDisplayedVerbosity = kLogVerbose; 17 | } 18 | return self; 19 | } 20 | -(void) logWithVerbosity:(LogVerbosity)verbosity msg:(NSString*)msg 21 | { 22 | [super logWithVerbosity:verbosity msg:msg]; 23 | if (verbosity <= self.maxDisplayedVerbosity && self.textView) { 24 | time_t now; 25 | time(&now); 26 | struct tm *timeinfo = localtime(&now); 27 | msg = [NSString stringWithFormat:@"\n%02d:%02d:%02d %@", 28 | timeinfo->tm_hour, 29 | timeinfo->tm_min, 30 | timeinfo->tm_sec, 31 | msg]; 32 | [self.textView setText:[self.textView.text stringByAppendingString:msg]]; 33 | } 34 | } 35 | @end 36 | -------------------------------------------------------------------------------- /Shared/UUID.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // UUID.cpp 3 | // Stetho 4 | // 5 | // Created by rsargent on 10/27/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | // System 10 | #include 11 | 12 | // Project 13 | #include "Nickname.h" 14 | 15 | // Self 16 | #include "UUID.h" 17 | 18 | UUID::UUID() {} 19 | 20 | UUID::UUID(void *data, size_t len) : uuid((const char*)data, len) {} 21 | 22 | UUID UUID::fromHex(const std::string &hex) { 23 | // TODO(rsargent): implement me 24 | assert(false); 25 | } 26 | 27 | std::string UUID::toHex() const { 28 | // TODO(rsargent): implement me 29 | assert(false); 30 | } 31 | 32 | std::string UUID::nickname() const { 33 | return computeNickname(uuid); 34 | } 35 | 36 | bool UUID::operator==(const UUID &rhs) const { 37 | return uuid == rhs.uuid; 38 | } 39 | 40 | bool UUID::operator!=(const UUID &rhs) const { 41 | return uuid != rhs.uuid; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Shared/UUID.h: -------------------------------------------------------------------------------- 1 | // 2 | // UUID.h 3 | // Stetho 4 | // 5 | // Created by rsargent on 10/27/12. 6 | // Copyright (c) 2012 BodyTrack. All rights reserved. 7 | // 8 | 9 | #ifndef __Stetho__UUID__ 10 | #define __Stetho__UUID__ 11 | 12 | #include 13 | 14 | class UUID { 15 | private: 16 | std::string uuid; 17 | public: 18 | UUID(); 19 | UUID(void *data, size_t len); 20 | static UUID fromHex(const std::string &hex); 21 | std::string toHex() const; 22 | std::string nickname() const; 23 | bool operator==(const UUID &rhs) const; 24 | bool operator!=(const UUID &rhs) const; 25 | }; 26 | 27 | #endif /* defined(__Stetho__UUID__) */ 28 | -------------------------------------------------------------------------------- /Shared/Utils.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "Utils.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | std::string string_vprintf(const char *fmt, va_list args) 10 | { 11 | int size= 500; 12 | char *buf = (char*)malloc(size); 13 | while (1) { 14 | va_list copy; 15 | va_copy(copy, args); 16 | #if defined(_WIN32) 17 | int nwritten= _vsnprintf(buf, size, fmt, copy); 18 | #else 19 | int nwritten= vsnprintf(buf, size, fmt, copy); 20 | #endif 21 | va_end(copy); 22 | // Some c libraries return -1 for overflow, some return 23 | // a number larger than size-1 24 | if (nwritten < 0) { 25 | size *= 2; 26 | } else if (nwritten >= size) { 27 | size = nwritten + 1; 28 | } else { 29 | if (nwritten && buf[nwritten-1] == 0) nwritten--; 30 | std::string ret(buf, buf+nwritten); 31 | free(buf); 32 | return ret; 33 | } 34 | buf = (char*)realloc(buf, size); 35 | } 36 | } 37 | 38 | std::string string_printf(const char *fmt, ...) 39 | { 40 | va_list args; 41 | va_start(args, fmt); 42 | std::string ret= string_vprintf(fmt, args); 43 | va_end(args); 44 | return ret; 45 | } 46 | 47 | unsigned long long microtime() 48 | { 49 | #ifdef WIN32 50 | return ((long long)GetTickCount() * 1000); 51 | #else 52 | struct timeval tv; 53 | gettimeofday(&tv, NULL); 54 | return ((long long) tv.tv_sec * (long long) 1000000 + 55 | (long long) tv.tv_usec); 56 | #endif 57 | } 58 | 59 | double doubletime() 60 | { 61 | return microtime() / 1000000.0; 62 | } 63 | 64 | std::vector glob(const std::string &pattern) { 65 | glob_t glob_result; 66 | glob(pattern.c_str(), GLOB_TILDE, NULL, &glob_result); 67 | std::vector ret; 68 | for (unsigned i = 0; i < glob_result.gl_pathc; i++) { 69 | ret.push_back(glob_result.gl_pathv[i]); 70 | } 71 | globfree(&glob_result); 72 | return ret; 73 | } 74 | 75 | 76 | //kern_return_t host_processor_info 77 | //( 78 | // host_t host, 79 | // processor_flavor_t flavor, 80 | // natural_t *out_processor_count, 81 | // processor_info_array_t *out_processor_info, 82 | // mach_msg_type_number_t *out_processor_infoCnt 83 | // ); 84 | 85 | #include 86 | #include 87 | #include 88 | 89 | void get_systemwide_cpu_usage(double &user_cpu_seconds_ret, double &system_cpu_seconds_ret, int &cpu_count_ret) 90 | { 91 | // This was determined empircally on an iPhone 5 running iOS 6. Is there a better way to find this number? 92 | double secondsPerTick = 1.0 / 100.0; 93 | processor_info_array_t cpuInfo; 94 | mach_msg_type_number_t numCpuInfo; 95 | natural_t numCPUs = 0; 96 | user_cpu_seconds_ret = system_cpu_seconds_ret = cpu_count_ret = 0; 97 | kern_return_t err = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &numCPUs, &cpuInfo, &numCpuInfo); 98 | if(err == KERN_SUCCESS) { 99 | cpu_count_ret = numCPUs; 100 | for(unsigned i = 0U; i < numCPUs; ++i) { 101 | natural_t *cpu = (natural_t*) &cpuInfo[CPU_STATE_MAX * i]; 102 | // Warning: these counters wrap in ~1.27 years 103 | user_cpu_seconds_ret += (cpu[CPU_STATE_USER] + cpu[CPU_STATE_NICE]) * secondsPerTick; 104 | system_cpu_seconds_ret += cpu[CPU_STATE_MAX] * secondsPerTick; 105 | } 106 | 107 | size_t cpuInfoSize = sizeof(integer_t) * numCpuInfo; 108 | vm_deallocate(mach_task_self(), (vm_address_t)cpuInfo, cpuInfoSize); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Shared/Utils.h: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.h 3 | // HeartRateMonitor 4 | // 5 | 6 | #ifndef HeartRateMonitor_Utils_h 7 | #define HeartRateMonitor_Utils_h 8 | 9 | #include 10 | #include 11 | #include 12 | std::string string_vprintf(const char *fmt, va_list args); 13 | 14 | std::string string_printf(const char *fmt, ...); 15 | 16 | unsigned long long microtime(); 17 | double doubletime(); 18 | 19 | std::vector glob(const std::string &pattern); 20 | 21 | void get_systemwide_cpu_usage(double &user_cpu_seconds_ret, double &system_cpu_seconds_ret, int &cpu_count_ret); 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /Shared/heartbeat_s1.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/Shared/heartbeat_s1.aiff -------------------------------------------------------------------------------- /Shared/heartbeat_s2.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/Shared/heartbeat_s2.aiff -------------------------------------------------------------------------------- /ZeoRelay/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // ZeoRelay 4 | // 5 | // Created by rsargent on 10/20/12. 6 | // Copyright (c) 2012 rsargent. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : NSObject 12 | 13 | @property (assign) IBOutlet NSWindow *window; 14 | 15 | @property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator; 16 | @property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel; 17 | @property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext; 18 | 19 | - (IBAction)saveAction:(id)sender; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /ZeoRelay/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // ZeoRelay 4 | // 5 | // Created by rsargent on 10/20/12. 6 | // Copyright (c) 2012 rsargent. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @implementation AppDelegate 12 | 13 | @synthesize persistentStoreCoordinator = _persistentStoreCoordinator; 14 | @synthesize managedObjectModel = _managedObjectModel; 15 | @synthesize managedObjectContext = _managedObjectContext; 16 | 17 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification 18 | { 19 | // Insert code here to initialize your application 20 | } 21 | 22 | // Returns the directory the application uses to store the Core Data store file. This code uses a directory named "org.bodytrack.ZeoRelay" in the user's Application Support directory. 23 | - (NSURL *)applicationFilesDirectory 24 | { 25 | NSFileManager *fileManager = [NSFileManager defaultManager]; 26 | NSURL *appSupportURL = [[fileManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject]; 27 | return [appSupportURL URLByAppendingPathComponent:@"org.bodytrack.ZeoRelay"]; 28 | } 29 | 30 | // Creates if necessary and returns the managed object model for the application. 31 | - (NSManagedObjectModel *)managedObjectModel 32 | { 33 | if (_managedObjectModel) { 34 | return _managedObjectModel; 35 | } 36 | 37 | NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"ZeoRelay" withExtension:@"momd"]; 38 | _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; 39 | return _managedObjectModel; 40 | } 41 | 42 | // Returns the persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it. (The directory for the store is created, if necessary.) 43 | - (NSPersistentStoreCoordinator *)persistentStoreCoordinator 44 | { 45 | if (_persistentStoreCoordinator) { 46 | return _persistentStoreCoordinator; 47 | } 48 | 49 | NSManagedObjectModel *mom = [self managedObjectModel]; 50 | if (!mom) { 51 | NSLog(@"%@:%@ No model to generate a store from", [self class], NSStringFromSelector(_cmd)); 52 | return nil; 53 | } 54 | 55 | NSFileManager *fileManager = [NSFileManager defaultManager]; 56 | NSURL *applicationFilesDirectory = [self applicationFilesDirectory]; 57 | NSError *error = nil; 58 | 59 | NSDictionary *properties = [applicationFilesDirectory resourceValuesForKeys:@[NSURLIsDirectoryKey] error:&error]; 60 | 61 | if (!properties) { 62 | BOOL ok = NO; 63 | if ([error code] == NSFileReadNoSuchFileError) { 64 | ok = [fileManager createDirectoryAtPath:[applicationFilesDirectory path] withIntermediateDirectories:YES attributes:nil error:&error]; 65 | } 66 | if (!ok) { 67 | [[NSApplication sharedApplication] presentError:error]; 68 | return nil; 69 | } 70 | } else { 71 | if (![properties[NSURLIsDirectoryKey] boolValue]) { 72 | // Customize and localize this error. 73 | NSString *failureDescription = [NSString stringWithFormat:@"Expected a folder to store application data, found a file (%@).", [applicationFilesDirectory path]]; 74 | 75 | NSMutableDictionary *dict = [NSMutableDictionary dictionary]; 76 | [dict setValue:failureDescription forKey:NSLocalizedDescriptionKey]; 77 | error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:101 userInfo:dict]; 78 | 79 | [[NSApplication sharedApplication] presentError:error]; 80 | return nil; 81 | } 82 | } 83 | 84 | NSURL *url = [applicationFilesDirectory URLByAppendingPathComponent:@"ZeoRelay.storedata"]; 85 | NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom]; 86 | if (![coordinator addPersistentStoreWithType:NSXMLStoreType configuration:nil URL:url options:nil error:&error]) { 87 | [[NSApplication sharedApplication] presentError:error]; 88 | return nil; 89 | } 90 | _persistentStoreCoordinator = coordinator; 91 | 92 | return _persistentStoreCoordinator; 93 | } 94 | 95 | // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) 96 | - (NSManagedObjectContext *)managedObjectContext 97 | { 98 | if (_managedObjectContext) { 99 | return _managedObjectContext; 100 | } 101 | 102 | NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; 103 | if (!coordinator) { 104 | NSMutableDictionary *dict = [NSMutableDictionary dictionary]; 105 | [dict setValue:@"Failed to initialize the store" forKey:NSLocalizedDescriptionKey]; 106 | [dict setValue:@"There was an error building up the data file." forKey:NSLocalizedFailureReasonErrorKey]; 107 | NSError *error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict]; 108 | [[NSApplication sharedApplication] presentError:error]; 109 | return nil; 110 | } 111 | _managedObjectContext = [[NSManagedObjectContext alloc] init]; 112 | [_managedObjectContext setPersistentStoreCoordinator:coordinator]; 113 | 114 | return _managedObjectContext; 115 | } 116 | 117 | // Returns the NSUndoManager for the application. In this case, the manager returned is that of the managed object context for the application. 118 | - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window 119 | { 120 | return [[self managedObjectContext] undoManager]; 121 | } 122 | 123 | // Performs the save action for the application, which is to send the save: message to the application's managed object context. Any encountered errors are presented to the user. 124 | - (IBAction)saveAction:(id)sender 125 | { 126 | NSError *error = nil; 127 | 128 | if (![[self managedObjectContext] commitEditing]) { 129 | NSLog(@"%@:%@ unable to commit editing before saving", [self class], NSStringFromSelector(_cmd)); 130 | } 131 | 132 | if (![[self managedObjectContext] save:&error]) { 133 | [[NSApplication sharedApplication] presentError:error]; 134 | } 135 | } 136 | 137 | - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender 138 | { 139 | // Save changes in the application's managed object context before the application terminates. 140 | 141 | if (!_managedObjectContext) { 142 | return NSTerminateNow; 143 | } 144 | 145 | if (![[self managedObjectContext] commitEditing]) { 146 | NSLog(@"%@:%@ unable to commit editing to terminate", [self class], NSStringFromSelector(_cmd)); 147 | return NSTerminateCancel; 148 | } 149 | 150 | if (![[self managedObjectContext] hasChanges]) { 151 | return NSTerminateNow; 152 | } 153 | 154 | NSError *error = nil; 155 | if (![[self managedObjectContext] save:&error]) { 156 | 157 | // Customize this code block to include application-specific recovery steps. 158 | BOOL result = [sender presentError:error]; 159 | if (result) { 160 | return NSTerminateCancel; 161 | } 162 | 163 | NSString *question = NSLocalizedString(@"Could not save changes while quitting. Quit anyway?", @"Quit without saves error question message"); 164 | NSString *info = NSLocalizedString(@"Quitting now will lose any changes you have made since the last successful save", @"Quit without saves error question info"); 165 | NSString *quitButton = NSLocalizedString(@"Quit anyway", @"Quit anyway button title"); 166 | NSString *cancelButton = NSLocalizedString(@"Cancel", @"Cancel button title"); 167 | NSAlert *alert = [[NSAlert alloc] init]; 168 | [alert setMessageText:question]; 169 | [alert setInformativeText:info]; 170 | [alert addButtonWithTitle:quitButton]; 171 | [alert addButtonWithTitle:cancelButton]; 172 | 173 | NSInteger answer = [alert runModal]; 174 | 175 | if (answer == NSAlertAlternateReturn) { 176 | return NSTerminateCancel; 177 | } 178 | } 179 | 180 | return NSTerminateNow; 181 | } 182 | 183 | @end 184 | -------------------------------------------------------------------------------- /ZeoRelay/ZeoRelay-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | org.bodytrack.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSApplicationCategoryType 26 | public.app-category.healthcare-fitness 27 | LSMinimumSystemVersion 28 | ${MACOSX_DEPLOYMENT_TARGET} 29 | NSHumanReadableCopyright 30 | Copyright © 2012 rsargent. All rights reserved. 31 | NSMainNibFile 32 | MainMenu 33 | NSPrincipalClass 34 | NSApplication 35 | 36 | 37 | -------------------------------------------------------------------------------- /ZeoRelay/ZeoRelay-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'ZeoRelay' target in the 'ZeoRelay' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /ZeoRelay/ZeoRelay.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | ZeoRelay.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /ZeoRelay/ZeoRelay.xcdatamodeld/ZeoRelay.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ZeoRelay/ZeoRelayTests/ZeoRelayTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | org.bodytrack.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ZeoRelay/ZeoRelayTests/ZeoRelayTests.h: -------------------------------------------------------------------------------- 1 | // 2 | // ZeoRelayTests.h 3 | // ZeoRelayTests 4 | // 5 | // Created by rsargent on 10/20/12. 6 | // Copyright (c) 2012 rsargent. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ZeoRelayTests : SenTestCase 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ZeoRelay/ZeoRelayTests/ZeoRelayTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // ZeoRelayTests.m 3 | // ZeoRelayTests 4 | // 5 | // Created by rsargent on 10/20/12. 6 | // Copyright (c) 2012 rsargent. All rights reserved. 7 | // 8 | 9 | #import "ZeoRelayTests.h" 10 | 11 | @implementation ZeoRelayTests 12 | 13 | - (void)setUp 14 | { 15 | [super setUp]; 16 | 17 | // Set-up code here. 18 | } 19 | 20 | - (void)tearDown 21 | { 22 | // Tear-down code here. 23 | 24 | [super tearDown]; 25 | } 26 | 27 | - (void)testExample 28 | { 29 | STFail(@"Unit tests are not implemented yet in ZeoRelayTests"); 30 | } 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /ZeoRelay/ZeoRelayTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /ZeoRelay/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /ZeoRelay/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /ZeoRelay/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // ZeoRelay 4 | // 5 | // Created by rsargent on 10/20/12. 6 | // Copyright (c) 2012 rsargent. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | return NSApplicationMain(argc, (const char **)argv); 14 | } 15 | -------------------------------------------------------------------------------- /appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/appicon.png -------------------------------------------------------------------------------- /appicon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtream/fluxtream-capture-ios/73d633970a63b57dbe7502f5fac10aa6b1667db6/appicon@2x.png --------------------------------------------------------------------------------