Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CoreBluetooth state preservation issue: willRestoreState not called in iOS 7.1

CoreBluetooth state preservation issue: willRestoreState not called in iOS 7.1

Hey all. I’ve been working on a Bluetooth LE project for the past few weeks, and hit a roadblock. I have been unable to get state restoration working properly in iOS 7 / 7.1. I’ve followed (I think) all of the steps Apple lays out, and got some clues on other stack overflow posts.

  1. I added the proper bluetooth permissions to the plist
  2. when I create my central manager, I give it a restore Identifier key.
  3. I always instantiate the CM with the same key
  4. I added the willRestoreState function to the CM delegate

My Test Case:

  1. Connect to peripheral
  2. Confirm connection
  3. Simulate Memory Eviction (kill(getpid(), SIGKILL);)
  4. Transmit Data

Results iOS 7:

The app would respond in the AppDelegate didFinishLaunchingWithOptions function, but the contents of the NSArray inside of launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey] was always an empty array.

Results on iOS 7.1:

Progress! I can see my CentralManager key in the UIApplicationLaunchOptionsBluetoothCentralsKey array 100% of the time, but willRestoreState is never called.

Code:

//All of this is in AppDelegate for testing

@import CoreBluetooth;
@interface AppDelegate () <CBCentralManagerDelegate, CBPeripheralDelegate>
@property (readwrite, nonatomic, strong) CBCentralManager *centralManager;
@end

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{

    self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:@{CBCentralManagerOptionRestoreIdentifierKey:@“myCentralManager”}];

    //Used to debug CM restore only
    NSArray *centralManagerIdentifiers = launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];
    NSString *str = [NSString stringWithFormat: @"%@ %lu", @"Manager Restores: ", (unsigned long)centralManagerIdentifiers.count];
    [self sendNotification:str];
    for(int i = 0;i<centralManagerIdentifiers.count;i++)
    {
        [self sendNotification:(NSString *)[centralManagerIdentifiers objectAtIndex:i]];
    }

    return YES;
}

- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary *)state {
    activePeripheral = [state[CBCentralManagerRestoredStatePeripheralsKey] firstItem];
    activePeripheral.delegate = self;

    NSString *str = [NSString stringWithFormat: @"%@ %lu", @"Device: ", activePeripheral.UUID];
    [self sendNotification:str];
}

//sendNotification is a func that creates a local notification for debugging when device connected to comp

When I run the tests, didFinishLaunchWithOptions is called 100% when my BLE device communicates to the phone when the app is not in memory, but willRestoreState is never called.

Any and all help would be great! thanks!

like image 536
cookieman459 Avatar asked Mar 14 '14 17:03

cookieman459


2 Answers

Okay, so I've had to delete two of my answers already to this question. But I think I've finally figured it out.

This comment is the key to your problem. Essentially, this centralManager:willRestoreState: only gets called if it's force closed by the OS while an outstanding operation is in progress with a peripheral (this does not include scanning for peripherals On further investigation, if you're scanning for a service UUID and the app is killed in the same way, or you've already finished connecting, it will in fact call your delegate).

To replicate: I have a peripheral using CoreBluetooth set up on my MacBook. I advertise on the peripheral, and have the central on my iPhone discover it. Then, leaving the OSX peripheral app running, kill your BT connection on your Mac and then initiate a connect from the central on your iOS device. This obviously will continuously run as the peripheral is non-reachable (apparently, the connection attempt can last forever as Bluetooth LE has no timeout on connections). I then added a button to my gui and hooked it up to a function in my view controller:

- (IBAction)crash:(id)sender
{
    kill(getpid(), SIGKILL);
}

This will kill the app as if it was killed by the OS. Once you are attempting to connect tap the button to crash the app (sometimes it takes two taps).

Activating Bluetooth on your Mac will then result in iOS relaunching your app and calling the correct handlers (including centralManager:willRestoreState:).

If you want to debug the handlers (by setting a breakpoint), in Xcode, before turning BT on on your Mac, set a breakpoint and then select 'Debug > Attach to Process... > By Process Identifier or Name...'.

In the dialog that appears, type the name of your app (should be identical to your target) and click "Attach". Xcode will then say waiting for launch in the status window. Wait a couple seconds and then turn on BT on OSX. Make sure your peripheral is still advertising and then iOS will pick it up and relaunch your app to handle the connection.

There are likely other ways to test this (using notify on a characteristic maybe?) but this flow is 100% reproducible so will likely help you test you code most easily.

like image 166
Sandy Chapman Avatar answered Oct 09 '22 11:10

Sandy Chapman


Had the same issue. From what I can work out, you need to use a custom dispatch queue when instantiating your CBCentralManager and your willRestoreState method will be triggered. I think this is due to async events not being handled by the default queue (when using "nil") when your app is started by the background recovery thread.

    ...
    dispatch_queue_t centralQueue = dispatch_queue_create("com.myco.cm", DISPATCH_QUEUE_SERIAL);

    cm = [[CBCentralManager alloc] initWithDelegate:self queue:centralQueue options:@{CBCentralManagerOptionRestoreIdentifierKey:@"cmRestoreID",CBCentralManagerOptionShowPowerAlertKey:@YES}];

    ...
like image 31
miniman42 Avatar answered Oct 09 '22 10:10

miniman42