Created
November 23, 2014 11:39
-
-
Save holmesal/104d1248482ee9caec44 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ESTransponder.m | |
// transponder | |
// | |
// Created by Alonso Holmes on 4/1/14. | |
// Copyright (c) 2014 Buildco. All rights reserved. | |
// | |
#import "ESTransponder.h" | |
#import <Firebase/Firebase.h> | |
//#import <Mixpanel/Mixpanel.h> | |
// Extensions | |
#import "CBCentralManager+Ext.h" | |
#import "CBPeripheralManager+Ext.h" | |
#import "CBUUID+Ext.h" | |
//#import "FCUser.h" | |
#define DEBUG_CENTRAL NO | |
#define DEBUG_PERIPHERAL NO | |
#define DEBUG_BEACON YES | |
#define DEBUG_USERS NO | |
#define DEBUG_TIMEOUTS NO | |
#define DEBUG_NOTIFICATIONS NO | |
#define IS_RUNNING_ON_SIMULATOR NO | |
#define TIMEOUT 30.0 // How old should a user be before I consider them gone? | |
#define MAX_BEACON 19 // How many beacons to use (IOS max 19) | |
#define REPORTING_INTERVAL 12.0 // How often to report to firebase | |
#define BACKGROUND_REPORTING_INTERVAL 3.0 // How often to report, when in the background | |
#define BEACON_TIMEOUT 10.0 // How long to range when a beacon is discovered (background only) | |
#define NOTIFICATION_TIMEOUT 1200.0 // Minimum time between sending discover notifications | |
#define CHIRP_LENGTH 10.0 // How long to chirp for? NOTE - might take up to 40 seconds more for other devices to exit the region | |
#define REPORT_FAILURE_IN_STACK_TIMEOUT 2.0f | |
@interface ESTransponder() <CBPeripheralManagerDelegate, CBCentralManagerDelegate, CLLocationManagerDelegate> | |
@property (nonatomic) BOOL bluetoothWasTried; | |
@property (nonatomic) BOOL coreLocationWasTried; | |
// Bluetooth / main class stuff | |
@property (strong, nonatomic) CBUUID *identifier; | |
@property (strong, nonatomic) CBCentralManager *centralManager; | |
@property (strong, nonatomic) CBPeripheralManager *peripheralManager; | |
@property (strong, nonatomic) NSDictionary *bluetoothAdvertisingData; | |
@property (strong, nonatomic) NSMutableDictionary *bluetoothUsers; | |
// Mixpanel | |
//@property (strong, nonatomic) Mixpanel *mixpanel; | |
@property (nonatomic, readonly) BOOL isDetecting; | |
@property (nonatomic, readonly) BOOL isBroadcasting; | |
// Beacon broadcasting | |
@property NSInteger flipCount; | |
@property BOOL currentlyChirping; | |
@property BOOL flippingBreaker; | |
@property BOOL isFlipping; | |
@property (strong, nonatomic) CLBeaconRegion *chirpBeaconRegion; | |
@property (strong, nonatomic) NSDictionary *chirpBeaconData; | |
@property (strong, nonatomic) NSDictionary *identityBeaconData; | |
// Beacon monitoring | |
@property (strong, nonatomic) CLLocationManager *locationManager; | |
@property (strong, nonatomic) NSMutableArray *regions; | |
@property (strong, nonatomic) NSArray *regionUUIDS; | |
@property (strong, nonatomic) CLBeaconRegion *rangingRegion; | |
@property (strong, nonatomic) NSTimer *rangingTimeout; | |
@property (nonatomic, readonly) BOOL hasSentErrorNote; | |
// Firebase-synced users array | |
@property (strong, nonatomic) Firebase *rootRef; | |
@property (strong, nonatomic) Firebase *earshotUsersRef; | |
@property (strong, nonatomic) NSMutableDictionary *earshotUsers; | |
@property (strong, nonatomic) NSTimer *filterTimer; | |
@property (strong, nonatomic) NSMutableDictionary *lastReported; | |
@property (assign, nonatomic) BOOL actuallyRemove; | |
@property (strong, nonatomic) NSDate *lastNotificationEvent; | |
//when timer is up, it fires a failure message for the stack. This must be interupted if success occurs sooner. | |
@property (strong, nonatomic) NSTimer *reportStackFailureTimer; | |
// Location filtering | |
@property (assign, nonatomic) BOOL okayToSendAnonymousNotification; | |
@property (strong, nonatomic) Firebase *seenRef; | |
@property (strong, nonatomic) NSMutableArray *seen; | |
// Oscillator | |
@property NSInteger broadcastMode; | |
@end | |
@implementation ESTransponder | |
@synthesize transponderID; | |
//@synthesize peripheralManagerIsRunning; | |
@synthesize isRunning; | |
static ESTransponder *sharedTransponder; | |
+(ESTransponder *)sharedInstance | |
{ | |
if (!sharedTransponder) | |
{ | |
sharedTransponder = [[ESTransponder alloc] init]; | |
} | |
return sharedTransponder; | |
} | |
//when timer is up, it fires a failure message for the stack. This must be interupted if success occurs sooner. | |
@synthesize reportStackFailureTimer; | |
-(ESTransponder*)init | |
{ | |
if (self = [super init]) | |
{ | |
// Generate a new id, or use an existing one | |
self.transponderID = [self getOrGenerateID]; | |
self.identifier = [CBUUID UUIDWithString:IDENTIFIER_STRING]; | |
self.bluetoothUsers = [[NSMutableDictionary alloc] init]; | |
self.transponderUsers = [[NSArray alloc] init]; | |
self.lastReported = [[NSMutableDictionary alloc] init]; | |
self.seen = [[NSMutableArray alloc] init]; | |
// self.mixpanel = [Mixpanel sharedInstance]; | |
// Set up the allowed beacon regions | |
// What are the uuids? | |
self.regionUUIDS = @[ @"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids", | |
@"make-your-own-uuids"]; | |
// Setup the firebase | |
[self initFirebase:@"https://transponder.firebaseio.com"]; | |
// Create the identity iBeacon | |
[self initIdentityBeacon:self.transponderID]; | |
// Start off NOT flipping between identity beacon / chirping beacon | |
self.currentlyChirping = NO; | |
// Start off okay to send anonymous notificatoins | |
self.okayToSendAnonymousNotification = YES; | |
// Start off with a broadcast mode of 0 | |
self.broadcastMode = 0; | |
// Start flipping between the identity beacon and BLE | |
[self startFlipping]; | |
// Chirp another beacona few times to wake up other users | |
// [self chirpBeacon]; | |
// Start the timer to filter the users | |
[self startFilterTimer]; | |
// Start a repeating timer to prune the in-range users, every 10 seconds | |
// [NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:@selector(pruneUsers) userInfo:nil repeats:YES]; | |
// Start a repeating timer to broadcast as an iBeacon, every 30 seconds | |
// Listen for chirpBeacon events | |
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(chirpBeacon) name:kTransponderTriggerChirpBeacon object:nil]; | |
// Listen for app sleep events | |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillEnterForeground) name:UIApplicationDidBecomeActiveNotification object:nil]; | |
// Listen for app wakeup events | |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillEnterBackground) name:UIApplicationWillResignActiveNotification object:nil]; | |
} | |
return self; | |
} | |
- (NSString *)getOrGenerateID | |
{ | |
// Check NSUserDefaults for a saved ID | |
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; | |
NSString *existingID = [prefs valueForKey:@"transponderID"]; | |
if (existingID){ | |
// Return the id | |
return existingID; | |
} else { | |
// Generate an id | |
NSInteger idInt = esRandomNumberIn(0, 99999999); | |
NSString *stringId = [NSString stringWithFormat:@"%ld",(long)idInt]; | |
[prefs setValue:stringId forKey:@"transponderID"]; | |
[prefs synchronize]; | |
return stringId; | |
} | |
} | |
// Shorthand for startDetecting, startBroadcasting, and chirpBeacon | |
- (void)startTransponder | |
{ | |
NSLog(@"Starting via startAwesome"); | |
[self startBroadcasting]; | |
[self startDetecting]; | |
[self chirpBeacon]; | |
} | |
// Send a local notification for deep background debugging | |
- (void)debugNote:(NSString *)text | |
{ | |
if (SHOW_DEBUG_NOTIFICATIONS) { | |
UILocalNotification *notice = [[UILocalNotification alloc] init]; | |
notice.alertBody = text; | |
notice.alertAction = @"Open"; | |
[[UIApplication sharedApplication] presentLocalNotificationNow:notice]; | |
} | |
} | |
- (void)pruneUsers | |
{ | |
// Only do this if you're in the foreground | |
UIApplication *application = [UIApplication sharedApplication]; | |
if (application.applicationState == UIApplicationStateActive) { | |
if (DEBUG_USERS) NSLog(@"Pruning BLE users!"); | |
// WHATTIMEISITRIGHTNOW.COM | |
NSDate *now = [[NSDate alloc] init]; | |
// Check every user | |
for(NSString *userBeaconKey in [self.bluetoothUsers.allKeys copy]) | |
{ | |
NSMutableDictionary *userBeacon = [self.bluetoothUsers objectForKey:userBeaconKey]; | |
// How long ago was this? | |
float lastSeen = [now timeIntervalSinceDate:[userBeacon objectForKey:@"lastSeen"]]; | |
if (DEBUG_USERS) NSLog(@"time interval for %@ -> %f",[userBeacon objectForKey:@"transponderID"],lastSeen); | |
// If it's longer than 20 seconds, they're probs gone | |
if (lastSeen > TIMEOUT) | |
{ | |
if (DEBUG_USERS) NSLog(@"Removing user: %@",userBeacon); | |
// Remove from earshotUsers, if it's actually in there | |
// if ([userBeacon objectForKey:@"transponderID"] != [NSNull null]) { | |
// [self removeUser:[userBeacon objectForKey:@"transponderID"]]; | |
// } | |
// Remove from bluetooth users | |
[self.bluetoothUsers removeObjectForKey:userBeaconKey]; | |
} else { | |
if (DEBUG_USERS) NSLog(@"Not removing user: %@",userBeacon); | |
} | |
} | |
} else { | |
if (DEBUG_USERS) NSLog(@"Not pruning BLE users - app is in the background"); | |
} | |
} | |
- (void)initFirebase:(NSString *)baseURL | |
{ | |
self.earshotUsers = [[NSMutableDictionary alloc] init]; | |
self.rootRef = [[Firebase alloc] initWithUrl:baseURL]; | |
self.earshotUsersRef = [[[self.rootRef childByAppendingPath:@"users"] childByAppendingPath:self.transponderID] childByAppendingPath:@"tracking"]; | |
[self.earshotUsersRef observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) | |
{ | |
// Update the locally-stored earshotUsers array | |
NSLog(@"Got data from firebase"); | |
NSLog(@"%@",snapshot.value); | |
if (snapshot.value != [NSNull null]){ | |
self.earshotUsers = [NSMutableDictionary dictionaryWithDictionary:snapshot.value]; | |
} else | |
{ | |
self.earshotUsers = [[NSMutableDictionary alloc] init]; | |
self.lastReported = [[NSMutableDictionary alloc] init]; | |
} | |
// Filter the users based on timeout | |
[self filterFirebaseUsers]; | |
// Emit the updated users | |
[self emitUsers]; | |
// The app icon badge listens to these events | |
// [[NSNotificationCenter defaultCenter] postNotificationName:kTransponderEventCountUpdated object:self userInfo:@{@"count":[NSNumber numberWithLong:[[self.earshotUsers allKeys] count]]}]; | |
}]; | |
// self.seenRef = [[[self.rootRef childByAppendingPath:@"users"] childByAppendingPath:self.transponderID] childByAppendingPath:@"seen"]; | |
// [self.seenRef observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) { | |
// if (snapshot.value != [NSNull null]){ | |
// self.seen = [NSMutableArray arrayWithArray:snapshot.value]; | |
// } else | |
// { | |
// self.seen = [[NSMutableArray alloc] init]; | |
// } | |
// }]; | |
} | |
- (void)emitUsers | |
{ | |
// Filter the current users into an array | |
NSMutableArray *formatted = [[NSMutableArray alloc] init]; | |
for (NSString *uuid in [self.earshotUsers allKeys]) | |
{ | |
NSNumber *lastSeen = [self.earshotUsers objectForKey:uuid]; | |
NSDictionary *formattedUser = @{@"uuid": uuid, | |
@"lastSeen": lastSeen}; | |
[formatted addObject:formattedUser]; | |
} | |
NSArray *output = [NSArray arrayWithArray:formatted]; | |
// Store the users | |
self.transponderUsers = output; | |
// Emit the users | |
[[NSNotificationCenter defaultCenter] postNotificationName:TransponderDidUpdateUsersInRange object:self userInfo:@{@"transponderUsers":output}]; | |
} | |
- (void)filterFirebaseUsers | |
{ | |
if (DEBUG_USERS) NSLog(@"Filtering firebase users with actuallyRemove = %d",self.actuallyRemove); | |
// Store the current time | |
NSDate *currentDate = [NSDate date]; | |
// Track whether a user to remove was found | |
BOOL removeUserInTheFuture = NO; | |
for (NSString *userKey in self.earshotUsers) { | |
// If the timeout is too old, clear it out | |
NSNumber *timestampNumber = [self.earshotUsers objectForKey:userKey]; | |
// Protect against weird values here | |
if ([timestampNumber isKindOfClass:[NSNumber class]]) { | |
long timestamp = [timestampNumber longValue]; | |
NSDate *beforeDate = [[NSDate alloc] initWithTimeIntervalSince1970:timestamp]; | |
NSTimeInterval interval = [currentDate timeIntervalSinceDate:beforeDate]; | |
NSLog(@"Long filter timeout for user %@ --> %f",userKey,interval); | |
if (interval > TIMEOUT) | |
{ | |
NSLog(@"Lost user %@ - has been too long: %f",userKey,interval); | |
if (self.actuallyRemove) { | |
// Remove the user | |
[self removeUser:userKey]; | |
} else{ | |
// Remove this user the next time through | |
removeUserInTheFuture = YES; | |
} | |
} | |
} else { | |
// There's a weird value here | |
[self removeUser:userKey]; | |
} | |
} | |
// If we just removed stuff, set actually remove back to NO for the regularly scheduled program | |
if (self.actuallyRemove) { | |
self.actuallyRemove = NO; | |
} | |
// If we found a user to remove, then set an interval for a few seconds from now and set actually remove to true | |
if (removeUserInTheFuture) { | |
// Chirp the beacon to see if you can get dem users back. | |
[self chirpBeacon]; | |
// set actually remove for the next run through | |
self.actuallyRemove = YES; | |
// Call this again in a couple of seconds, at which point the user will be actually removed | |
[self performSelector:@selector(filterFirebaseUsers) withObject:nil afterDelay:10.0]; | |
} | |
} | |
- (void)startFilterTimer | |
{ | |
if (self.filterTimer) { | |
[self.filterTimer invalidate]; | |
} | |
self.filterTimer = [NSTimer timerWithTimeInterval:TIMEOUT target:self selector:@selector(filterFirebaseUsers) userInfo:nil repeats:YES]; | |
[[NSRunLoop mainRunLoop] addTimer:self.filterTimer forMode:NSDefaultRunLoopMode]; | |
} | |
// Takes in a bluetooth or iBeacon user and adds it to earshotUsers | |
- (void)addUser:(NSString *)userID | |
{ | |
// NSLog(@"Adding user to firebase: %@",userID); | |
// // Get the rounded date/time | |
// uint rounded = [self roundTime:[[NSDate date] timeIntervalSince1970]]; | |
// // Add the user for yourself | |
// [[self.earshotUsersRef childByAppendingPath:userID] setValue:[[NSNumber alloc] initWithInt:rounded]]; | |
// // Add yourself for the user | |
// [[[[[self.rootRef childByAppendingPath:@"users"] childByAppendingPath:userID] childByAppendingPath:@"tracking"] childByAppendingPath:self.transponderID] setValue:[[NSNumber alloc] initWithInt:rounded]]; | |
// [self.lastReported setObject:[[NSNumber alloc] initWithDouble:now] forKey:userID]; | |
// NSLog(@"Rounded time is %d",rounded); | |
uint now = [[NSDate date] timeIntervalSince1970]; | |
// Make sure it's not the time we already have | |
NSNumber *last = [self.lastReported objectForKey:userID]; | |
uint then = [last intValue]; | |
// NSLog(@"Time difference for user %@ is %u",userID,(now - then)); | |
uint howLong = now - then; | |
if (howLong > REPORTING_INTERVAL) | |
{ | |
if(DEBUG_USERS) NSLog(@"Adding/updating user on firebase: %@",userID); | |
// Add the user for yourself | |
[[self.earshotUsersRef childByAppendingPath:userID] setValue:[[NSNumber alloc] initWithInt:now]]; | |
// Add yourself for the user | |
[[[[[self.rootRef childByAppendingPath:@"users"] childByAppendingPath:userID] childByAppendingPath:@"tracking"] childByAppendingPath:self.transponderID] setValue:[[NSNumber alloc] initWithInt:now]]; | |
[self.lastReported setObject:[[NSNumber alloc] initWithInt:now] forKey:userID]; | |
// Also add them to the seen array if you haven't before | |
// Only send if we haven't seen this user before | |
NSUInteger index = [self.seen indexOfObject:userID]; | |
if (index == NSNotFound) | |
{ | |
// Add this user as seen | |
[self.seen addObject:userID]; | |
// Sync to firebase | |
// [self.seenRef setValue:self.seen]; | |
} | |
} else { | |
if(DEBUG_TIMEOUTS) NSLog(@"Timeout not long enough, doing nothing."); | |
} | |
} | |
- (uint)roundTime:(NSTimeInterval)time | |
{ | |
// Round to the nearest 5 seconds | |
// NSLog(@"time = %f", time); | |
double rounded = REPORTING_INTERVAL * floor((time/REPORTING_INTERVAL)+0.5); | |
return rounded; | |
} | |
- (void)removeUser:(NSString *)userID | |
{ | |
#warning not sure this is the right way to handle removing users... | |
#warning - add feature to not remove this user if it exists elsewhere in the bluetooth array | |
// Only do this if you're in the foreground | |
UIApplication *application = [UIApplication sharedApplication]; | |
if (application.applicationState == UIApplicationStateActive) { | |
// Remove the user for yourself | |
[[self.earshotUsersRef childByAppendingPath:userID] removeValue]; | |
} | |
// Remove yourself for the user | |
// [[[[[self.rootRef childByAppendingPath:@"users"] childByAppendingPath:userID] childByAppendingPath:@"tracking"] childByAppendingPath:self.transponderID] removeValue]; | |
} | |
# pragma mark - FOREGROUND vs BACKGROUND modes | |
- (void)appWillEnterForeground | |
{ | |
NSLog(@"Transponder -- App is entering foreground"); | |
// Start ranging beacons in Region 19 | |
[self.locationManager startRangingBeaconsInRegion:self.rangingRegion]; | |
// Start flipping between an iBeacon and a BLE peripheral | |
// If you aren't already | |
if (!self.isFlipping) [self startFlipping]; | |
// Chirp the discovery iBeacon for a few seconds | |
[self chirpBeacon]; | |
// Update the date we use for the notification timeout | |
self.lastNotificationEvent = [NSDate date]; | |
} | |
- (void)appWillEnterBackground | |
{ | |
NSLog(@"Transponder -- App is entering background"); | |
// Stop chirping as a beacon | |
[self stopChirping]; | |
// Start advertising only as a BLE peripheral | |
[self stopFlipping]; | |
// Stop ranging beacons | |
[self stopRanging]; | |
// Pause the filter timer | |
// self.filterTimer | |
} | |
# pragma mark - core bluetooth | |
- (void)startDetecting | |
{ | |
NSLog(@"startDetecting called"); | |
// Setup beacon monitoring for regions | |
[self setupBeaconRegions]; | |
// Listen for major location changes | |
[self.locationManager startMonitoringSignificantLocationChanges]; | |
// Listen for bluetooth LE | |
[self startDetectingTransponders]; | |
} | |
- (void)startBroadcasting | |
{ | |
NSLog(@"startBroadcast called"); | |
[self startBluetoothBroadcast]; | |
} | |
- (void)startDetectingTransponders | |
{ | |
if (!self.centralManager) | |
NSLog(@"New central created"); | |
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil]; | |
// Uncomment this timer if you need to report ranges in a timer | |
// detectorTimer = [NSTimer scheduledTimerWithTimeInterval:UPDATE_INTERVAL target:self | |
// selector:@selector(reportRanges:) userInfo:nil repeats:YES]; | |
} | |
- (void)startBluetoothBroadcast | |
{ | |
// start broadcasting if it's stopped | |
if (!self.peripheralManager) { | |
[self debugNote:@"Transponder is booting bluetooth + iBeacon"]; | |
self.peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil]; | |
} | |
} | |
- (void)startScanning | |
{ | |
NSDictionary *scanOptions = @{CBCentralManagerScanOptionAllowDuplicatesKey:@(YES)}; | |
[self.centralManager scanForPeripheralsWithServices:@[self.identifier] options:scanOptions]; | |
_isDetecting = YES; | |
if (DEBUG_CENTRAL) NSLog(@"Scanning!"); | |
} | |
- (void)startAdvertising | |
{ | |
self.bluetoothAdvertisingData = @{CBAdvertisementDataServiceUUIDsKey:@[self.identifier], CBAdvertisementDataLocalNameKey:self.transponderID}; | |
// Start advertising over BLE | |
[self debugNote:@"Transponder is broadcasting"]; | |
[self.peripheralManager startAdvertising:self.bluetoothAdvertisingData]; | |
} | |
#pragma mark - CBCentralManagerDelegate | |
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral | |
advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI | |
{ | |
if (DEBUG_CENTRAL) { | |
NSLog(@"did discover peripheral: %@, data: %@, %1.2f", [peripheral.identifier UUIDString], advertisementData, [RSSI floatValue]); | |
CBUUID *uuid = [advertisementData[CBAdvertisementDataServiceUUIDsKey] firstObject]; | |
NSLog(@"service uuid: %@", [uuid representativeString]); | |
} | |
// Create a user if there isn't one | |
NSMutableDictionary *existingUser = [self.bluetoothUsers objectForKey:[peripheral.identifier UUIDString]]; | |
if ([existingUser count] == 0) { | |
// No user yet, make one | |
NSMutableDictionary *newUser = [[NSMutableDictionary alloc] initWithDictionary:@{@"lastSeen": [[NSDate alloc] init], | |
@"transponderID": [NSNull null]}]; | |
// Insert | |
[self.bluetoothUsers setObject:newUser forKey:[peripheral.identifier UUIDString]]; | |
// Alias | |
existingUser = newUser; | |
// Send a local notification to tell the user we discovered a device | |
[self sendAnonymousNotification]; | |
// Send the new (anonymous) user notification | |
// [[NSNotificationCenter defaultCenter] postNotificationName:kTransponderEventNewUserDiscovered object:self userInfo:@{@"user":existingUser}]; | |
// Chirp the beacon! | |
[self chirpBeacon]; | |
} else{ | |
// Update the time last seen | |
[existingUser setObject:[[NSDate alloc] init] forKey:@"lastSeen"]; | |
} | |
// Update local name if included in advertisement | |
NSString *localName = [advertisementData valueForKey:@"kCBAdvDataLocalName"]; | |
if (localName){ | |
[existingUser setValue:localName forKey:@"transponderID"]; | |
// Add to transponder users | |
} | |
// If it has a local name (whether just set or actively being broadcast), call addUser | |
NSString *userID = [existingUser objectForKey:@"transponderID"]; | |
if (userID && userID != (NSString*)[NSNull null]) | |
{ | |
// NSLog(@"%@ addUser %@ <centralManager:didDiscoverPeripheral:advertisementData:RSSI:>", [FCUser owner].id, userID); | |
[self addUser:userID]; | |
[self sendUserDiscoverEvent:userID]; | |
} else { | |
[self sendAnonymousUserDiscoverEvent]; | |
} | |
if (DEBUG_CENTRAL) NSLog(@"%@",self.bluetoothUsers); | |
// Notify peeps that an transponder user was discovered | |
// [[NSNotificationCenter defaultCenter] postNotificationName:kTransponderEventTransponderUserDiscovered | |
// object:self | |
// userInfo:@{@"user":existingUser, | |
// @"identifiedUsers":self.earshotUsers, | |
// @"bluetoothUsers":self.bluetoothUsers}]; | |
} | |
- (void)centralManagerDidUpdateState:(CBCentralManager *)central | |
{ | |
if (DEBUG_CENTRAL) NSLog(@"-- central state changed: %@", self.centralManager.stateString); | |
// Emit the state, if state != unknown | |
if (central.state) | |
{ | |
self.bluetoothWasTried = YES; | |
[self emitBluetoothState]; | |
} | |
// If powered on, start scanning | |
if (central.state == CBCentralManagerStatePoweredOn) | |
{ | |
[self startScanning]; | |
} | |
} | |
#pragma mark - CBPeripheralManagerDelegate | |
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral | |
{ | |
if (DEBUG_PERIPHERAL) NSLog(@"-- peripheral state changed: %@", peripheral.stateString); | |
// Emit the state if stat is known. | |
if (peripheral.state) | |
{ | |
self.bluetoothWasTried = YES; | |
[self emitBluetoothState]; | |
} | |
// If powered on, start scanning | |
if (peripheral.state == CBPeripheralManagerStatePoweredOn) | |
{ | |
[self startAdvertising]; | |
} | |
} | |
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error | |
{ | |
if (DEBUG_PERIPHERAL) | |
{ | |
if (error) | |
NSLog(@"error starting advertising: %@", [error localizedDescription]); | |
else | |
NSLog(@"did start advertising!"); | |
} | |
} | |
#pragma mark - iBeacon broadcasting | |
// Setup the beacon responsible for communicating the user's transponder ID | |
- (void)initIdentityBeacon:(NSString *)userID | |
{ | |
// Convert the userID into a major and minor value to transmit | |
NSLog(@"Decomposing UUID %@",userID); | |
uint16_t major, minor; | |
esDecomposeIdToMajorMinor([userID intValue], &major, &minor); | |
NSLog(@"Got major: %hu and minor:%hu",major,minor); | |
CLBeaconRegion *identityBeaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString: IDENTITY_BEACON_UUID] | |
major:major | |
minor:minor | |
identifier:[NSString stringWithFormat:@"Broadcast region %d",19]]; | |
self.identityBeaconData = [identityBeaconRegion peripheralDataWithMeasuredPower:nil]; | |
} | |
// Below lie the functions for interacting with iBeacon | |
- (void)chirpBeacon | |
{ | |
NSLog(@"chirpBeacon called!"); | |
NSLog(@"Is stack running? %u", [self isRunning]); | |
UIApplication *application = [UIApplication sharedApplication]; | |
if ([application applicationState] == UIApplicationStateActive) { | |
// Don't do anything if you're already chirping | |
if (self.currentlyChirping == YES) { | |
if (DEBUG_BEACON) NSLog(@"Currently chirping, creation CANCELLED"); | |
} else{ | |
if (DEBUG_BEACON) NSLog(@"Attempting to create new beacon!"); | |
if (DEBUG_BEACON) NSLog(@"Current regions: %@",self.regions); | |
// Build an array to sort | |
NSMutableArray *fucker = [[NSMutableArray alloc] init]; | |
for (NSNumber *isInside in self.regions) { | |
NSDictionary *bullshit = @{@"some": [[NSDate alloc] init],@"isInside":isInside}; | |
[fucker addObject:bullshit]; | |
} | |
// Preticate - filter self.regions | |
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(SELF.isInside == %@)", @NO]; | |
NSArray *availableNos = [fucker filteredArrayUsingPredicate:predicate]; | |
if (DEBUG_BEACON) NSLog(@"Available regions count: %lu",(unsigned long)[availableNos count]); | |
if ([availableNos count]) | |
{ | |
NSInteger randomChoice = esRandomNumberIn(0, (int)[availableNos count]); | |
id aNo = [availableNos objectAtIndex:randomChoice]; | |
NSUInteger chosenIndex = [availableNos indexOfObject:aNo]; | |
NSString *regionUUID = [self.regionUUIDS objectAtIndex:chosenIndex]; | |
if (DEBUG_BEACON) NSLog(@"Creating a new chirping beacon broadcast region in slot number %lu -> %@",(unsigned long)chosenIndex, regionUUID); | |
self.chirpBeaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString: regionUUID] | |
major:0 | |
minor:0 | |
identifier:[NSString stringWithFormat:@"Broadcast region %@",regionUUID]]; | |
self.chirpBeaconData = [self.chirpBeaconRegion peripheralDataWithMeasuredPower:nil]; | |
// Start chirping | |
self.currentlyChirping = YES; | |
// Stop chirping after 10 seconds | |
[self performSelector:@selector(stopChirping) withObject:nil afterDelay:CHIRP_LENGTH]; | |
// This region should be off-limits for a bit | |
[self disallowRegion:chosenIndex]; | |
} else | |
{ | |
// Not chirping | |
self.currentlyChirping = NO; | |
int timeoutSeconds = 10; | |
NSLog(@"Couldn't find an open region, trying again in %i seconds.",timeoutSeconds); | |
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeoutSeconds*1000* NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ | |
// note - it's okay if this fires in the background | |
// this only sets the beacon data and the currentlyChirping flag, but it will be overridden by the flippingBreaker flag if the app is in the background | |
[self chirpBeacon]; | |
}); | |
} | |
} | |
} else{ | |
if (DEBUG_BEACON) NSLog(@"Application isn't in the foreground - not creating a beacon"); | |
} | |
} | |
- (void)stopChirping | |
{ | |
if (DEBUG_BEACON) NSLog(@"Stopping chirping!"); | |
self.currentlyChirping = NO; | |
} | |
- (void)disallowRegion:(NSUInteger)regionNumber | |
{ | |
if (DEBUG_BEACON) NSLog(@"Disallowing region %lu",(unsigned long)regionNumber); | |
[self.regions replaceObjectAtIndex:regionNumber withObject:@YES]; | |
// In a while, re-allow the region | |
[self performSelector:@selector(allowRegion:) withObject:[NSNumber numberWithInteger:regionNumber] afterDelay:30]; | |
} | |
- (void)allowRegion:(NSNumber *)regionNumber | |
{ | |
if (DEBUG_BEACON) NSLog(@"Re-enabling region %@",regionNumber); | |
[self.regions replaceObjectAtIndex:[regionNumber integerValue] withObject:@NO]; | |
} | |
- (void)startFlipping | |
{ | |
// If you're in the foreground, start flipping the state | |
if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) | |
{ | |
self.isFlipping = YES; | |
self.broadcastMode = 0; | |
self.flippingBreaker = NO; | |
[self flipState]; | |
} else { | |
// You're in the background, so just start broadcasting on BLE | |
[self debugNote:@"Transponder is broadcasting in the background."]; | |
[self resetBluetooth]; | |
} | |
} | |
- (void)stopFlipping | |
{ | |
// Set the flag | |
self.isFlipping = NO; | |
// Stop flipState from continuing to flip | |
self.flippingBreaker = YES; | |
// Reset the bluetooth right now to broadcast BLE | |
if (DEBUG_BEACON) NSLog(@"-- broadcasting as BLE"); | |
[self resetBluetooth]; | |
// Just in case, reset the bluetooth stack again in a few seconds (to miss any timeouts in the meantime) | |
// [self performSelector:@selector(resetBluetooth) withObject:nil afterDelay:1.5]; | |
} | |
- (void)flipState | |
{ | |
if (IS_RUNNING_ON_SIMULATOR) | |
return; | |
// There are three states: | |
// State 0: Broadcasting using normal BLE | |
// State 1: Broadcasting as an iBeacon on a wakeup region (0-18) | |
// State 2: Broadcasting as an iBeacon as this device on Region 19 | |
// ^ optional, only available is self.discoveryBeacon == @YES | |
if (!self.flippingBreaker) { | |
// Increment the broadcast mode | |
self.broadcastMode++; | |
// Reset it if necessary | |
if (self.broadcastMode > 2) { | |
self.broadcastMode = 0; | |
} | |
// Check the broadcast mode | |
switch (self.broadcastMode) { | |
case 0: | |
// Start broadcasting using normal bluetooth low energy | |
if (DEBUG_BEACON) NSLog(@"-- broadcasting as BLE"); | |
[self resetBluetooth]; | |
break; | |
case 1: | |
// Is this flag set? | |
if (self.currentlyChirping == YES) { | |
// Start broadcasting as a wakeup region | |
if (DEBUG_BEACON) NSLog(@"-- broadcasting as chirp iBeacon"); | |
[self startBeacon:self.chirpBeaconData]; | |
} else{ | |
// Broadcast as normal BLE | |
if (DEBUG_BEACON) NSLog(@"-- broadcasting as BLE (no chirp fallback)"); | |
[self resetBluetooth]; | |
} | |
// Start broadcasting on a wakeup region | |
break; | |
case 2: | |
// Start broadcasting as an iBeacon on identity beacon | |
if (DEBUG_BEACON) NSLog(@"-- broadcasting as identity iBeacon"); | |
[self startBeacon:self.identityBeaconData]; | |
break; | |
default: | |
break; | |
} | |
// Do this again after a while | |
[self performSelector:@selector(flipState) withObject:nil afterDelay:1.0]; | |
} | |
} | |
- (void)resetBluetooth | |
{ | |
// Stop what you're doing and advertise with bluetooth | |
[self.peripheralManager stopAdvertising]; | |
[self.peripheralManager startAdvertising:self.bluetoothAdvertisingData]; | |
} | |
// Start broadcasting as an iBeacon. Works with either the identity or wakeup beacon data | |
- (void)startBeacon:(NSDictionary *)beaconData | |
{ | |
// Stop what you're doing and advertise as a beacon | |
[self.peripheralManager stopAdvertising]; | |
// Broadcast | |
[self.peripheralManager startAdvertising:beaconData]; | |
} | |
# pragma mark - iBeacon discovery | |
// K this shit gets crazy so stay with me. We're going to listen for "chirps" (broadcasts under 10 seconds for the purpose of wakeup) on regions 0-18. Region 19 is special. Region 19 is where we will actually range beacons. Therefore a phone can chirp on region 10, and ranging will start on region 19. | |
- (void)setupBeaconRegions | |
{ | |
NSLog(@"Setting up beacon regions..."); | |
// Make the location manager | |
self.locationManager = [[CLLocationManager alloc] init]; | |
// Set the delegate | |
self.locationManager.delegate = self; | |
// BOOL what = [CLLocationManager isMonitoringAvailableForClass:[CLBeacon class]]; | |
// int [CLLocationManager] auth | |
// Init the region tracker | |
self.regions = [[NSMutableArray alloc] init]; | |
// Log the regions already monitored | |
for (CLRegion *monitored in [self.locationManager monitoredRegions]){ | |
NSLog(@"Already monitoring region: %@", monitored); | |
// [self.locationManager stopMonitoringForRegion:monitored]; | |
} | |
// Regions 0-18 are available for wakeup chirps | |
for (int major=0; major< MAX_BEACON; major++) { | |
NSString *regionUUID = [self.regionUUIDS objectAtIndex:major]; | |
if (DEBUG_BEACON) NSLog(@"Starting to monitor for region %@",regionUUID); | |
// Start outside the region | |
[self.regions addObject:@NO]; | |
// Create a region with this minor | |
CLBeaconRegion *region = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString: regionUUID] | |
identifier:[NSString stringWithFormat:@"Listen region %@",regionUUID]]; | |
// Wake up the app when you enter this region | |
// region.notifyEntryStateOnDisplay = YES; | |
region.notifyOnEntry = YES; | |
region.notifyOnExit = YES; | |
// Start monitoring via location manager | |
[self.locationManager startMonitoringForRegion:region]; | |
// OPTIONAL - if we need to initialize this region with an inside/outside state, do it here | |
[self.locationManager requestStateForRegion:region]; | |
} | |
// Region 19 is available for ranging - totally separate | |
// This might look like duplicate code, but it's way easier to understand if this gets set up as a separate region | |
if (DEBUG_BEACON) NSLog(@"Setting up the mystical region %@",IDENTITY_BEACON_UUID); | |
self.rangingRegion = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString: IDENTITY_BEACON_UUID] | |
identifier:[NSString stringWithFormat:@"Identity region %@",IDENTITY_BEACON_UUID]]; | |
// self.rangingRegion = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString: IBEACON_UUID] | |
// identifier:[NSString stringWithFormat:@"Minor mod region:%d",19]]; | |
// Why not also wake up when we enter this region | |
// self.rangingRegion.notifyEntryStateOnDisplay = YES; | |
self.rangingRegion.notifyOnEntry = YES; | |
self.rangingRegion.notifyOnExit = YES; | |
// Start monitoring via location manager | |
[self.locationManager startMonitoringForRegion:self.rangingRegion]; | |
// OPTIONAL - if we need to initialize this region with an inside/outside state, do it here | |
[self.locationManager requestStateForRegion:self.rangingRegion]; | |
// Start ranging for beacons in this region | |
[self.locationManager startRangingBeaconsInRegion:self.rangingRegion]; | |
} | |
- (void)stopRanging | |
{ | |
[self.locationManager stopRangingBeaconsInRegion:self.rangingRegion]; | |
} | |
#pragma mark - CLLocationManagerDelegate | |
//- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region | |
//{ | |
// NSLog(@"AHH DID EENTER REGION"); | |
//} | |
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status | |
{ | |
// Emit that shit, unless underetmined state. | |
if (status == kCLAuthorizationStatusNotDetermined) | |
{ | |
return; | |
} | |
self.coreLocationWasTried = YES; | |
[self emitBluetoothState]; | |
} | |
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLBeaconRegion *)region | |
{ | |
// What region? | |
NSUUID *uuidVal = [region proximityUUID]; | |
NSString *uuid = [uuidVal UUIDString]; | |
if(DEBUG_BEACON) NSLog(@"Got region event with UUID: %@",uuid); | |
NSUInteger indexOfThisRegion = [self.regionUUIDS indexOfObject:uuid]; | |
// NSLog(@"Got state %ld for region %lu", state, (unsigned long)indexOfThisRegion); | |
// NSLog(@"Got state %li for region %@ : %@",state,minor,region); | |
switch (state) { | |
case CLRegionStateInside: | |
// Update the beacon regions dictionary if it's not Region 19 | |
if (![uuid isEqual: IDENTITY_BEACON_UUID]) { | |
if (indexOfThisRegion != NSNotFound){ | |
[self.regions replaceObjectAtIndex:indexOfThisRegion withObject:@YES]; | |
} | |
} | |
// Regardless, start ranging on Region 19 | |
[self.locationManager startRangingBeaconsInRegion:self.rangingRegion]; | |
// Kill any existing timeouts | |
if (self.rangingTimeout) { | |
[self.rangingTimeout invalidate]; | |
} | |
// If we're in the background, don't do this forever | |
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) { | |
// Make a new timer | |
self.rangingTimeout = [NSTimer timerWithTimeInterval:BEACON_TIMEOUT target:self selector:@selector(stopRanging) userInfo:nil repeats:NO]; | |
// Actually start the timer | |
[[NSRunLoop mainRunLoop] addTimer:self.rangingTimeout forMode:NSDefaultRunLoopMode]; | |
} | |
// Send a local notification to tell the user we discovered a device | |
[self sendAnonymousNotification]; | |
if (DEBUG_BEACON){ | |
NSLog(@"--- Entered region: %@", region); | |
[self debugNote:[NSString stringWithFormat:@"Entered region %lu",(unsigned long)indexOfThisRegion]]; | |
NSLog(@"%@",self.regions); | |
} | |
break; | |
case CLRegionStateOutside: | |
if (![uuid isEqual: IDENTITY_BEACON_UUID]) { | |
if (indexOfThisRegion != NSNotFound){ | |
[self.regions replaceObjectAtIndex:indexOfThisRegion withObject:@NO]; | |
} | |
} | |
if (DEBUG_BEACON){ | |
NSLog(@"--- Exited region: %@", region); | |
// [self debugNote:[NSString stringWithFormat:@"Exited region %lu",(unsigned long)indexOfThisRegion]]; | |
NSLog(@"%@",self.regions); | |
} | |
break; | |
case CLRegionStateUnknown: | |
if (DEBUG_BEACON) NSLog(@"Region %@ in unknown state - doing nothing...",uuid); | |
break; | |
default: | |
NSLog(@"This is never supposed to happen."); | |
break; | |
} | |
// Doing this for some reason scans forever... | |
// [self.locationManager startRangingBeaconsInRegion:self.rangingRegion]; | |
} | |
-(void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region | |
{ | |
if ([beacons count] != 0){ | |
if (DEBUG_BEACON) NSLog(@"Ranged beacons from identity region!"); | |
if (DEBUG_BEACON) NSLog(@"%@",beacons); | |
} | |
for (CLBeacon *beacon in beacons) { | |
// NSString *userID = [NSString stringWithFormat:@"%@",beacon.minor]; | |
uint32_t recomposed; | |
esRecomposeMajorMinorToId([beacon.major intValue], [beacon.minor intValue], &recomposed); | |
// NSLog(@"Recomposed major: %@ and minor:%@ -> %d",beacon.major,beacon.minor,recomposed); | |
NSString *userID = [NSString stringWithFormat:@"%u",recomposed]; | |
// Send a non-anonymous notification (or try to, at least) | |
[self sendNonanonymousNotification:userID]; | |
// if (DEBUG_BEACON) NSLog(@"%@ addUser %@ <locationManager:didRangeBeacons:inRegion:>", [FCUser owner].id, userID); | |
[self addUser:userID]; | |
} | |
} | |
- (ESTransponderStackState)isRunning | |
{ | |
if (self.coreLocationWasTried && self.bluetoothWasTried) | |
{ | |
if (self.peripheralManager.state == CBPeripheralManagerStatePoweredOn && | |
self.centralManager.state == CBPeripheralManagerStatePoweredOn && | |
[CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorized) | |
{ | |
return ESTransponderStackStateActive; | |
} else | |
{ | |
return ESTransponderStackStateDisabled; | |
} | |
} | |
return ESTransponderStackStateUnknown; | |
} | |
- (void)emitBluetoothState | |
{ | |
if (self.coreLocationWasTried && self.bluetoothWasTried) | |
{ | |
if (self.peripheralManager.state == CBPeripheralManagerStatePoweredOn && | |
self.centralManager.state == CBPeripheralManagerStatePoweredOn && | |
[CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorized) | |
{ | |
if (reportStackFailureTimer) | |
{ | |
[reportStackFailureTimer invalidate]; | |
reportStackFailureTimer = nil; | |
} | |
[[NSNotificationCenter defaultCenter] postNotificationName:TransponderEnabled object:nil]; | |
} else | |
{ | |
if (!reportStackFailureTimer) | |
{ | |
reportStackFailureTimer = [NSTimer timerWithTimeInterval:REPORT_FAILURE_IN_STACK_TIMEOUT target:self selector:@selector(reportStackFailureTimerAction:) userInfo:nil repeats:NO]; | |
[[NSRunLoop mainRunLoop] addTimer:reportStackFailureTimer forMode:NSDefaultRunLoopMode]; | |
} | |
} | |
} | |
} | |
-(void)reportStackFailureTimerAction:(NSTimer*)theTimer | |
{ | |
[reportStackFailureTimer invalidate]; | |
reportStackFailureTimer = nil; | |
//emit the failure notification | |
[[NSNotificationCenter defaultCenter] postNotificationName:TransponderDisabled object:nil]; | |
} | |
-(CLLocation*)getLocation | |
{ | |
return self.locationManager.location; | |
} | |
- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error | |
{ | |
NSLog(@"Region monitoring failed for region: %@", region); | |
NSLog(@"Region monitoring failed with error: %@", [error localizedDescription]); | |
// If we haven't already sent an error, send one | |
if (SHOW_DEBUG_NOTIFICATIONS && !self.hasSentErrorNote) { | |
[self debugNote:@"iBeacons have broken down"]; | |
} | |
} | |
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error | |
{ | |
NSLog(@"ERROR - %@",error); | |
} | |
- (void)locationManager:(CLLocationManager *)manager didStartMonitoringForRegion:(CLRegion *)region | |
{ | |
NSLog(@"Started monitoring for region: %@",region); | |
} | |
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations | |
{ | |
NSLog(@"Did update location significantly!"); | |
self.okayToSendAnonymousNotification = YES; | |
} | |
- (void)sendAnonymousNotification | |
{ | |
if (self.okayToSendAnonymousNotification){ | |
NSLog(@"%@ YES", NSStringFromSelector(_cmd)); | |
[self sendDiscoverNotification]; | |
self.okayToSendAnonymousNotification = NO; | |
} else | |
{ | |
NSLog(@"%@ NO", NSStringFromSelector(_cmd)); | |
[self debugNote:@"NOT sending anon note - already sent"]; | |
} | |
} | |
- (void)sendNonanonymousNotification:(NSString *)userID | |
{ | |
// Only send if we haven't seen this user before | |
if ([self.seen indexOfObject:userID] != NSNotFound) { | |
// Attempt to send the notification | |
[self sendDiscoverNotification]; | |
} else { | |
[self debugNote:@"NOT sending anon note - user already seen"]; | |
} | |
} | |
# pragma mark - local notifications | |
- (void)sendDiscoverNotification | |
{ | |
// The app icon badge listens to these events | |
if ([[self.earshotUsers allKeys] count] == 0){ | |
// Badge the number of bluetooth users | |
// [[NSNotificationCenter defaultCenter] postNotificationName:kTransponderEventCountUpdated object:self userInfo:@{@"count":[NSNumber numberWithLong:[[self.bluetoothUsers allKeys] count]]}]; | |
// [[NSNotificationCenter defaultCenter] postNotificationName:kTransponderEventCountUpdated object:self userInfo:@{@"count":@1}]; | |
} else { | |
// Badge the number of users tracked via firebase | |
// [[NSNotificationCenter defaultCenter] postNotificationName:kTransponderEventCountUpdated object:self userInfo:@{@"count":[NSNumber numberWithLong:[[self.earshotUsers allKeys] count]]}]; | |
} | |
// Only do this if the app is in the background | |
// NSLog(@"Current app state is %ld",[[UIApplication sharedApplication] applicationState]); | |
UIApplication *app = [UIApplication sharedApplication]; | |
if ([app applicationState] == UIApplicationStateBackground) | |
{ | |
// If there aren't any existing discover notifications | |
NSArray *notificationArray = [app scheduledLocalNotifications]; | |
if ([notificationArray count] == 0) | |
{ | |
// If it's been more than 20 minutes since the last notification OR app open | |
NSDate *currentDate = [NSDate date]; | |
NSTimeInterval howLong = [currentDate timeIntervalSinceDate:self.lastNotificationEvent]; | |
if (isnan(howLong) || howLong > NOTIFICATION_TIMEOUT) { | |
NSLog(@"Sending a local discover notification!"); | |
// Cancel all of the existing notifications | |
// [app cancelAllLocalNotifications]; | |
// Add a new notification | |
// UILocalNotification *notice = [[UILocalNotification alloc] init]; | |
// notice.alertBody = [NSString stringWithFormat:@"Shortwave users nearby!"]; | |
// notice.alertAction = @"Converse"; | |
// [app scheduleLocalNotification:notice]; | |
[[NSNotificationCenter defaultCenter] postNotificationName:TransponderSuggestsDiscoveryNotification object:nil]; | |
// Update the date we use for the notification timeout | |
self.lastNotificationEvent = [NSDate date]; | |
// Track this via mixpanel | |
// [self.mixpanel track:@"Notified of user nearby" properties:@{}]; | |
// Suggest that the app notify a user | |
} else{ | |
NSLog(@"It has only been %f seconds of the %f second notification timeout - ignoring notification call.", howLong, NOTIFICATION_TIMEOUT); | |
[self debugNote:@"NOT sending disc note - timeout too short"]; | |
} | |
} else { | |
NSLog(@"There is already an existing discover notification - ignoring notification call"); | |
} | |
} else | |
{ | |
NSLog(@"App is not in the background - ignoring notication call."); | |
} | |
} | |
# pragma mark - transponder events | |
- (void)sendUserDiscoverEvent:(NSString *)uuid | |
{ | |
NSDictionary *userInfo = @{@"uuid": uuid}; | |
[[NSNotificationCenter defaultCenter] postNotificationName:TransponderUserDiscovered object:nil userInfo:userInfo]; | |
} | |
- (void)sendAnonymousUserDiscoverEvent | |
{ | |
[[NSNotificationCenter defaultCenter] postNotificationName:TransponderAnonymousUserDiscovered object:nil]; | |
} | |
-(void)dealloc | |
{ | |
[[NSNotificationCenter defaultCenter] removeObserver:self]; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment