I have xamarin project. I would like to pair bluetooth button with an app and keep connection alive in suspended state. I successfully subscribed to characteristic event which represent the click in foreground state. The main use case is to handle the event in suspended state and send data to a server.
I read the documentation here, but I am having the difficulties implement restoring the CBCentralManager especially translating these methods into Xamairn.iOS.
Opt In to State Preservation and Restoration
myCentralManager =
[[CBCentralManager alloc] initWithDelegate:self queue:nil
options:#{ CBCentralManagerOptionRestoreIdentifierKey:
#"myCentralManagerIdentifier" }];
Reinstantiate Your Central and Peripheral Managers
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSArray *centralManagerIdentifiers =
launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];
...
Implement the Appropriate Restoration Delegate Method
- (void)centralManager:(CBCentralManager *)central
willRestoreState:(NSDictionary *)state {
NSArray *peripherals =
state[CBCentralManagerRestoredStatePeripheralsKey];
...
Questions:
Does app handle the events in suspended state? (technically in backgrounded)
Do I have to reconnect the device after app gets restored?
I think you can find the answer in the document.
1.Does app handle the events in suspended state? (technically in
backgrounded)
You need to enable a Core Bluetooth background execution mode in the info.plist to ensure your app keep running in background.
Also, an iOS app linked on or after iOS 10.0 must include in its Info.plist file the usage description keys for the types of data it needs to access or it will crash. InfoPlistKeyReference
You can read: Core Bluetooth Background Execution Modes
2.Do I have to reconnect the device after app gets restored?
If you gets restored here means you restart your app, I think you need to reconnect since it will lose the connection after the app is terminated.
If you gets restored here means enter foreground from background, I don't think you need to reconnect if you enabled background mode as your app is still running in the background.
Related
First of all the question what is the best way of using core bluetooth in the central role to send data to a bluetooth LE devices. The data need to be processed and that takes enough time to cause problems on the UI thread if it runs on it. The user will initiate the process with the phone app open and then either keep using the app or close the app and expect the data to continue sending to the device.
I have found 2 really bad ways of doing it which seem to work
Put the bluetooth CBCentralManager objet on the main queue and risk blocking the UI
Ignore the indications from the iOS bluetooth stack that its not ready to transmit and risk loosing data.
This seems to have its roots in both iOS threading/dispatch queues as well as iOS Bluetooth internals.
The bluetooth LE iOS application connects to a bluetooth LE device as central role. The CBCentralManager is initialized according to apples documentation. The queue defined as:
The dispatch queue to use to dispatch the central role events. If the
value is nil, the central manager dispatches central role events using
the main queue.
As suggested by vladiulianbogdan answer to Swift: Choose queue for Bluetooth Central manager we should be creating a serial queue for the CBCentralManager. This seems to make sense and for a while I was following this advice. Also allprog comment to Swift CoreBluetooth: Should CentralManager run in a separate thread
suggests that the main queue will be suspended but other queues will not, which is the opposite of what I am seeing.
While using a serial queue for bluetooth, and using a different one than the main thread would be preferable. There is a problem: The callback:
-(void)peripheralIsReadyToSendWriteWithoutResponse:(CBPeripheral *)peripheral
{
[self sendNextBluetoothLePacket];
}
stop getting called. There is another way to check if the peripheral is ready to send more data, CBPeripheral has a member variable canSendWriteWithoutResponse which returns true if its ok to send. This variable also begins retuning false and never goes back to true.
I found this comment from Sandeep Bhandari which says that all of the queue threads are stopped when the app goes in the background unless they are one of the background modes provided by apple. Biniou found that he was able to solve his core bluetooth background issue by initializing in a view controller instead of the app delegate. This does not make sense to me.
My app does have the core-bluetooth background mode selected in its info.plist so it should quality as one of those background modes. What I find is that when my app goes in the background the app does continue process data. I see log messages from polling loops that run every 100 milliseconds.
If I trigger bluetooth LE writes from those polling loops I am able to keep sending data. The problem is I am unable to determine a safe rate to send the data and either its very slow or data is sometimes lost.
Im not sure how to best deal with this. Any suggestions would be appreciated. It seems like no matter what I do when I go in to the background I loose my ability to determine if its safe to send data.
I see this comment that indicates that the only solution would be to change how the bluetooth device connects to the phone, is this the case? Im not sure changing the hardware is an option for me at this point.
The ideal solution would be to find a way to put CBCentralManager on its own serial queue but create that queue in such a way that the queue was not stopped when the app goes in to the background. If someone knows how to do that I believe it would solve my problem.
The way my current code is goes like this. When the bluetooth service is created in the applicationDidFinishLaunchingWithOptions callback to my AppDelegate
self.cm = [[CBCentralManager alloc] initWithDelegate:self
queue:nil
options:#{CBCentralManagerOptionShowPowerAlertKey:#(0)}];
Or with the serial queue which should work but is not
dispatch_queue_t bt_queue = dispatch_queue_create("BT_queue", 0);
self.cm = [[CBCentralManager alloc] initWithDelegate:self
queue:bt_queue
options:#{CBCentralManagerOptionShowPowerAlertKey:#(0)}];
When its time to send some data I use one of these
[self.cbPeripheral writeValue:data
forCharacteristic:self.rxCharacteristic
type:CBCharacteristicWriteWithoutResponse];
If I just keep calling this writeValue with no delay in between without trying to check if its safe to send data. It will eventually fail.
Extra background execution time is requested once the connection is established with this code
- (void) centralManager:(CBCentralManager *)central
didConnectPeripheral:(CBPeripheral *)peripheral
{
UIApplication *app = [UIApplication sharedApplication];
if (self.globalBackgroundTask != UIBackgroundTaskInvalid) {
[app endBackgroundTask:self.globalBackgroundTask];
self.globalBackgroundTask = UIBackgroundTaskInvalid;
}
self.globalBackgroundTask = [app beginBackgroundTaskWithExpirationHandler:^{
[app endBackgroundTask:_globalBackgroundTask];
_globalBackgroundTask = UIBackgroundTaskInvalid;
}];
Any thoughts on this or suggestions as to how I could give CBCentralManager a queue that was not on the UI thread and would not get shut down when the app goes in to the background would be greatly appreciated. If thats not possible I need to try and pick one of the workarounds.
I started messing around with the Watch OS framework today and wanted to throw together a quick app, but have come to a couple questions.
I made an iOS app that just shows the current battery % as well as the state of the battery. I then wanted to show that over on the watch.
The only time the watch app will update is when I totally close the iOS app, then open it, while the watch app is active. How do I allow my watch app to be updated if I open it after the iOS app has been opened?
This kind of goes with number 2. But how do I allow the watch app to fetch info from the iOS app, after it has been in the background? As an example, lets say the iOS app has been in the background and I wanted to fetch the battery % without opening the iOS app to the foreground.
Some side notes on how I set this up -
Within the iOS app, in the viewDidLoad method, I start my session.
if ([WCSession isSupported]) {
wcSession = [WCSession defaultSession];
wcSession.delegate = self;
[wcSession activateSession];
}
Then call my method to update the actual battery % and state. Within that method, I have this which sends the info over to the watch:
NSDictionary *message = #{
#"message" : [NSString stringWithFormat:#"%#", [numberFormatter stringFromNumber:levelObj]],
#"message_2" : [NSString stringWithFormat:#"%ld",(long)[UIDevice currentDevice].batteryState],
};
[wcSession sendMessage:message replyHandler:nil errorHandler:^(NSError * _Nonnull error) {
NSLog(#"%#", error.localizedDescription);
}];
I also call this same method in the viewDidAppear, so I don't have to relaunch the app completely, to allow refreshing of the watch counterpart.
On the watch side I have the viewWillActivate method with the same activation as the iOS side as well as the method to handle what the watch app receives from the iOS side. But it will only update when I restart the iOS app fully.
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *,id> *)message {
NSLog(#"Message recieved!");
[_batteryLevelLabelW setText:message[#"message"]];
}
Also in there is the code to handle the battery state message, which is a bit long.
I hope I gave a good amount of information to help.
According to documentation:
Use the sendMessage(_:replyHandler:errorHandler:) or
sendMessageData(_:replyHandler:errorHandler:) method to
transfer data to a reachable counterpart. These methods are intended
for immediate communication between your iOS app and WatchKit
extension. The isReachable property must currently be true for these
methods to succeed.
If watchapp is not foreground, message will not be delivered since isReachable is false.
Method you should use is updateApplicationContext(_:) - it will wait till watch app will be opened at foreground and only then will be delivered.
I am trying to maintain a MultipeerConnectivity "session" when the application enters temporarily in the background, so I thought about using a background task as I've seen few times here ... The problem is I have no idea how to "maintain" the session with the UIBackgroundTask, can someone please post a hint
I don't care about the advertisers/browsers, it's okay to stop them, but I'd like the session to not disconnect as reconnecting is super buggy for the moment.
As per apple documentation "If the app moves into the background, the framework stops advertising and browsing and disconnects any open sessions. Upon returning to the foreground, the framework automatically resumes advertising and browsing, but the developer must reestablish any closed sessions" Refer: Apple doc
One way of extending the connection is as follows
Answering my own question, hoping it would help people in the same situation.
For people new to iOS development, "using a background service" simple means turning on the "Background Modes" option in the "Capabilities" tab of your target.
That alone should give your app around 10 minutes life in the background before it gets killed.
But, when the app goes to background, I use the "backgroundTimeRemaining" to know how much time I have left, it just starts at 180 (in sec, so 3 minutes), yet, the printing loop did continue to work passed three minutes, which means there is a need to manually code what should happen when the time is reached.
For Multipeer Connectivity, this is enough to maintain the connection alive when the app enters background, and it will still receive all messages/streams without a problem.
For the sake of stability, I do some cleaning as follow:
In the appDelegate.h
#property (nonatomic) UIBackgroundTaskIdentifier backgroundTask; //declaring a background task
In the appDelegate.m
- (void)applicationDidEnterBackground:(UIApplication *)application
{
self.backgroundTask = [application beginBackgroundTaskWithExpirationHandler:^
{
//This is called 3 seconds before the time expires
//Here: Kill the session, advertisers, nil its delegates,
// which should correctly send a disconnect signal to other peers
// it's important if we want to be able to reconnect later,
// as the MC framework is still buggy
[application endBackgroundTask:self.backgroundTask];
self.backgroundTask = UIBackgroundTaskInvalid; //Invalidate the background task
}];
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// Here: We should init back the session, start the advertising and set the delegates from scratch
// This should allow the app to reconnect to the same session with more often than not
self.backgroundTask = UIBackgroundTaskInvalid; //Here we invalidate the background task if the timer didn't end already
}
I've asked this same question once on the apple developer forums. One of the Apple employees told me that basically all of the Multipeer connectivity should be considered off-limits when your app is not int the foreground.
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.
I added the proper bluetooth permissions to the plist
when I create my central manager, I give it a restore Identifier key.
I always instantiate the CM with the same key
I added the willRestoreState function to the CM delegate
My Test Case:
Connect to peripheral
Confirm connection
Simulate Memory Eviction (kill(getpid(), SIGKILL);)
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!
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.
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}];
...
You need to move the CentralManager to its own queue.
You also need to instantiate your peripheral with a restore identifier.
I have a voip app and it needs to run in the background. To my understanding these are the things I need to do:
Flag the app as voip.
Set the 'application does not run in background' flag to NO.
Set an expiration handler, a piece of code that extends the standard 10 minutes of execution time you get.
More?
I set both flags in the info.plist file and I get my 10 minutes. I tried what is suggested in this post. Here is my code:
//in didFinishLaunchingWithOptions:
expirationHandler = ^{
NSLog(#"ending background task");
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
NSLog(#"restarting background task");
bgTask = UIBackgroundTaskInvalid;
bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:expirationHandler];
NSLog(#"finished running background task");
};
//in applicationDidEnterBackground
NSLog(#"entering background mode");
bgTask = UIBackgroundTaskInvalid;
bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:expirationHandler];
// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// inform others to stop tasks, if you like
[[NSNotificationCenter defaultCenter] postNotificationName:#"MyApplicationEntersBackground" object:self];
//this while loop is just here for testing
inBackground = true;
while (inBackground) {
NSLog(#"stayin alive!!"); //this keeps going forever
sleep(10);
}
});
The situation:
I use a third party library that handles the communication with our webservice. The service is a CommuniGate pro server. I receive presence updates (online/offline) and instant messages from contacts via the library. The library is CommuniGate's ximss library, a protocol they made which is similar to xmpp and is used for xml-based sip requests, as well as IM and presence. When the user logs in to the app, he sees his contacts (CmmuniGate friends list) and he can choose to call one. After a ximss verification message has been sent and the other side accepted the call it logs the start time of the call and starts a facetime call.
The problem:
When the app enters the background by pressing the home button, I start seeing the 'stayin alive' message in the log and every ten minutes I see that it restarts the background task.
When the app enters the background by pressing the power button, the 'staying alive' messages start showing up for ten minutes, after that it restarts the background task and start restarting it about every 50-100 miliseconds.
I would've been fine with this for now, even it eats battery, because I have time to work on updates and our users don't own the ipads, we do. The problem for me now is that the ximss library loses it's connection (it is session-based). I could restart the session in the library, but this means quite a bit of data transfer to fetch the contacts list and some users use 3g.
I can't edit the library's source, nor can I see it, so I don't know if it creates the sockets the right way.
What do I have to do to handle both situations correctly? I don't even understand why there is a difference.
You cannot re-extend background tasks like this; your app is likely to be terminated. If this is working, it's because you have the background voip mode enabled, not because you are restarting the background task.
Once you have set the voip plist entry, iOS will attempt to keep your app alive as long as possible and restart it if it does get terminated. From Implementing a VoIP App:
Including the voip value in the UIBackgroundModes key lets the system
know that it should allow the app to run in the background as needed
to manage its network sockets. An app with this key is also relaunched
in the background immediately after system boot to ensure that the
VoIP services are always available.
In addition to setting this key, if you need to periodically run code to keep your voip connection alive, you can use the setKeepAliveTimeout:handler: method on UIApplication.
See also Tips for Developing a VoIP App:
There are several requirements for implementing a VoIP app:
Add the UIBackgroundModes key to your app’s Info.plist file. Set the value of this key to an array that includes the voip string.
Configure one of the app’s sockets for VoIP usage.
Before moving to the background, call the setKeepAliveTimeout:handler: method to install a handler to be
executed periodically. Your app can use this handler to maintain its
service connection.
Configure your audio session to handle transitions to and from active use.
To ensure a better user experience on iPhone, use the Core Telephony framework to adjust your behavior in relation to cell-based
phone calls; see Core Telephony Framework Reference.
To ensure good performance for your VoIP app, use the System Configuration framework to detect network changes and allow your app
to sleep as much as possible.
Almost all of the documentation you need is on the Apple developer site.