iOS 13 started to request Bluetooth permission. When bluetooth permission isn't granted yet, I want to show a custom screen to explain why I need the Bluetooth and suggest to give the app an access to it. Before that I have to check if bluetooth permission is granted or not.
This function immediately shows native popup and asks for the permission:
public bool NeedsBluetoothPermission()
{
if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0))
{
return CBCentralManager.Authorization != CBManagerAuthorization.AllowedAlways;
}
else
{
return false;
}
}
Question: How to check if the app needs to request bluetooth permissions without triggering native popup first?
This answer doesn't work for me, as I don't create any instance of CBCentralManager yet, I only use its static property.
Native iOS developers, please chime in too. I guess it's not Xamarin problem only...
Another way is to use CBCentralInitOptions, where you can set "ShowPowerAlert" as false.
When you create the instance of CBCentralManager, pass the init option and it won't show a native popup
Finally, I figured this out.
This behavior was actual for iOS 13.0 beta.
For the latest iOS 13.2 I don't observe this issue.
I'm able to check CBCentralManager.Authorization property silently.
The system pop-up shows up when I create an instance of CBCentralManager.
In my case, CBCentralManager.Authorization does not exist. But CBPeripheralManager.authorization is a static class var that works with iOS 13+. Apple's documentation
- (void)checkBluetoothPermissionStatus:(CDVInvokedUrlCommand *)command
{
if (#available(iOS 13.1, *)) {
switch(CBPeripheralManager.authorization) {
case CBPeripheralManagerAuthorizationStatusAuthorized:
NSLog(#"CBP BLE allowedAlways");
[self requestBluetoothPermission: command]; // Further get the BLE on/off status once the permission is granted
break;
case CBPeripheralManagerAuthorizationStatusDenied:
NSLog(#"CBP BLE denied");
[self sendMessage:#"Denied" error:true command:command];
break;
case CBPeripheralManagerAuthorizationStatusNotDetermined:
NSLog(#"CBP BLE notDetermined");
[self sendMessage:#"Undetermined" error:true command:command];
break;
case CBPeripheralManagerAuthorizationStatusRestricted:
NSLog(#"CBP BLE restricted");
[self sendMessage:#"Restricted" error:true command:command];
break;
}
} else {
// Fallback on earlier versions
NSLog(#"CBP BLE unknown");
[self sendMessage:#"Unknown" error:true command:command];
}
}
Related
Is there any way for a Cocoa application to detect when the user has tried to quit it via its Dock menu, and not by some other method?
Normally it's possible to catch and respond to quit events using the application delegate's applicationShouldTerminate: method. However, this method doesn't seem to distinguish between the request to quit coming from the application's main menu, from its Dock icon, from an Apple event, or any other conventional method of quitting the application. I'm curious if there's any way to know precisely how the user has tried to quit the application.
It is in fact possible for an app to know the reason why it's quitting by checking to see if there is current AppleEvent being handled and, if so, checking to see whether it's a quit event and whether it was the Dock that sent it. (See this thread discussing how to tell if an app is being quit because the system is logging out or shutting down.)
Here is an example of a method that, when called from the application delegate's applicationShouldTerminate: method, will return true if the app is being quit via the Dock:
- (bool)isAppQuittingViaDock {
NSAppleEventDescriptor *appleEvent = [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent];
if (!appleEvent) {
// No Apple event, so the app is not being quit by the Dock.
return false;
}
if ([appleEvent eventClass] != kCoreEventClass || [appleEvent eventID] != kAEQuitApplication) {
// Not a 'quit' event
return false;
}
NSAppleEventDescriptor *reason = [appleEvent attributeDescriptorForKeyword:kAEQuitReason];
if (reason) {
// If there is a reason for this 'quit' Apple event (such as the current user is logging out)
// then it didn't occur because the user quit the app through the Dock.
return false;
}
pid_t senderPID = [[appleEvent attributeDescriptorForKeyword:keySenderPIDAttr] int32Value];
if (senderPID == 0) {
return false;
}
NSRunningApplication *sender = [NSRunningApplication runningApplicationWithProcessIdentifier:senderPID];
if (!sender) {
return false;
}
return [#"com.apple.dock" isEqualToString:[sender bundleIdentifier]];
}
On iOS, I only can check state of Bluetooth. I'm find the solutions on network and use it.
public class CallBluetoothIphoneService : ICallBlueTooth
{
public void LaunchBluetoothOnPhone()
{
try
{
// Is bluetooth enabled?
var bluetoothManager = new CBCentralManager();
if (bluetoothManager.State == CBCentralManagerState.PoweredOff|| bluetoothManager.State == CBCentralManagerState.Unknown)
// Does not go directly to bluetooth on every OS version though, but opens the Settings on most
UIApplication.SharedApplication.OpenUrl(new NSUrl("App-Prefs:root=Bluetooth"));
}
catch (Exception ex)
{
Console.WriteLine(ex);
throw;
}
}
}
But when I try turn off Bluetooth and test code, state of bluetooth is "Unknown".
Then I run code, device open settings, toggle button has color green (turn on bluetooth), but when I check state of Bluetooth in code, State of Bluetooth is "Unknown", is not "Power on".
I'm using Xamarin 3.3 and test on device iOS version 12.0.
I am not sure exactly what you want to do, but if your intent is to open the Bluetooth settings page, this:
UIApplication.SharedApplication.OpenUrl(new NSUrl("App-Prefs:root=Bluetooth"));
won't work. Apple has at some points allowed this (iOS 8 IIRC) and at other points it has disallowed this (most versions of iOS). See this long SO thread about this issue: How to open Settings programmatically like in Facebook app?
Regardless, there is no need. When iOS detects that your app has created a CBCentralManager type with delegate, iOS will display an alert to the user that allows them to go to the bluetooth settings to enable bluetooth by tapping the "Settings" button in the alert.
As far as always getting state as "Unknown", you need to check the state in the delegate for the CBCentralManager. You cannot use the parameterless CBCentralManager constructor new CBCentralManager();. Check the apple docs: https://developer.apple.com/documentation/corebluetooth/cbcentralmanager?language=objc and note that there are only two listed init methods, one that takes delegate and queue parameters, and one that takes delegate, queue, and options parameters, although no one complains if you use the parameterless constructor... but you will never get the correct state if you use it. See: https://stackoverflow.com/a/36824770/2913599
So try this:
public class CallBluetoothIphoneService : ICallBluetooth
{
public void LaunchBluetoothOnPhone()
{
try
{
// Is bluetooth enabled?
var bluetoothManager = new CBCentralManager(new MySimpleCBCentralManagerDelegate(), DispatchQueue.CurrentQueue);
// This will always show state "Unknown". You need to check it in the delegate's UpdatedState method
Console.WriteLine($"State: {bluetoothManager.State.ToString()}");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
public class MySimpleCBCentralManagerDelegate : CBCentralManagerDelegate
{
override public void UpdatedState(CBCentralManager mgr)
{
// You can check the state in this delegate method
Console.WriteLine($"UpdatedState: {mgr.State.ToString()}");
if (mgr.State == CBCentralManagerState.PoweredOn)
{
//Passing in null scans for all peripherals. Peripherals can be targeted by using CBUIIDs
CBUUID[] cbuuids = null;
mgr.ScanForPeripherals(cbuuids); //Initiates async calls of DiscoveredPeripheral
//Timeout after 30 seconds
var timer = new Timer(30 * 1000);
timer.Elapsed += (sender, e) => mgr.StopScan();
}
else
{
//Invalid state -- Bluetooth powered down, unavailable, etc.
System.Console.WriteLine("Bluetooth is not available");
}
}
public override void DiscoveredPeripheral(CBCentralManager central, CBPeripheral peripheral, NSDictionary advertisementData, NSNumber RSSI)
{
Console.WriteLine("Discovered {0}, data {1}, RSSI {2}", peripheral.Name, advertisementData, RSSI);
}
}
Bottom line: always create a CBCentralManager object with one of the following constructors:
CBCentralManager(ICBCentralManagerDelegate, DispatchQueue)
CBCentralManager(ICBCentralManagerDelegate, DispatchQueue, CBCentralInitOptions)
I have the following scenario: iOS app (peripheral) X OSX app (central)
I instantiate my peripheral manager with CBPeripheralManagerOptionRestoreIdentifierKey.
In my peripheral's didFinishLaunchingWithOptions I send a local notification after getting a peripheral with UIApplicationLaunchOptionsBluetoothPeripheralsKey (don't do anything with it)
In my peripheral's willRestoreState I also trigger a notification (don't do anything other than that)
If my peripheral app is still running in the background before it gets killed due to memory pressure, I get messages from the OSX central just fine.
After the iOS app gets killed, when OSX central sends a message, both notifications mentioned above come through on iOS, but the message I was actually expecting doens't.
I've not resintantiated my peripheralManager at any moment, where and how should I do it? I only have one peripheralManager for the entire cycle of my app.
Any suggestions are welcome.
UPDATE:
if do
let options: Dictionary = [CBPeripheralManagerOptionRestoreIdentifierKey: "myId"]
peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: options)
in willRestoreState, my apps just lose connection
Right, after having re-viseted all topics on this matter 100 times I've finally figured it out, here is exactly how it should be implemented:
in AppDelegate's didFinishLaunchingWithOptions:
if let options = launchOptions {
if let peripheralManagerIdentifiers: NSArray = options[UIApplicationLaunchOptionsBluetoothPeripheralsKey] as? NSArray {
//Loop through peripheralManagerIdentifiers reinstantiating each of your peripheralManagers
//CBPeripheralManager(delegate: corebluetooth, queue: nil, options: [CBPeripheralManagerOptionRestoreIdentifierKey: "identifierInArray"])
}
else {
//There are no peripheralManagers to be reinstantiated, instantiate them as you normally would
}
}
else {
//There is nothing in launchOptions, instantiate them as you normally would
}
At this point, willRestoreState should start getting called, however, there will be no centrals in your array of centrals if you have one to manage all centrals subscribed to your characteristics. Since all my centrals are always subscribed to all of my characteristics in one single service I have, I simply loop though all subscribedCentrals in any of the characteristics re-adding them to my array of centrals.
Please modify this according to your needs.
In willRestoreState:
var services = dict[CBPeripheralManagerRestoredStateServicesKey] as! [CBMutableService]
if let service = services.first {
if let characteristic = service.characteristics?.first as? CBMutableCharacteristic {
for subscribedCentral in characteristic.subscribedCentrals! {
self.cbCentrals.append(subscribedCentral as! CBCentral)
}
}
}
After this you should be ok to call any methods you have prepared to talk to any central, even if you deal with several of them simultaneously.
Good luck!
I'm trying to use FSEvents in my sandboxed app to monitor some directories. I implemented the following class:
#implementation SNTracker
- (id)initWithPaths:(NSArray *)paths {
self=[super init];
if (self) {
trackedPaths=paths;
CFTimeInterval latency=1.0;
FSEventStreamContext context={0,(__bridge void *)self,NULL,NULL,NULL};
FSEventStreamRef eeventStream=FSEventStreamCreate(kCFAllocatorDefault,&callback,&context,(__bridge CFArrayRef)trackedPaths,kFSEventStreamEventIdSinceNow,latency,kFSEventStreamCreateFlagUseCFTypes|kFSEventStreamCreateFlagWatchRoot|kFSEventStreamCreateFlagFileEvents);
FSEventStreamScheduleWithRunLoop(eeventStream,[[NSRunLoop mainRunLoop] getCFRunLoop],kCFRunLoopDefaultMode);
FSEventStreamStart(eeventStream);
}
return self;
}
static void callback(ConstFSEventStreamRef streamRef,void *clientCallBackInfo,size_t numEvents,void *eventPaths,const FSEventStreamEventFlags eventFlags[],const FSEventStreamEventId eventIds[]) {
NSLog(#"asd");
}
The problem is that "asd" never gets printed (i.e. the callback function is never called). When I disable "Enable App Sandboxing" in the Summary of my main target in Xcode, the callback gets called. Am I doing something wrong? The only entitlement I've given to the sandboxed app is read-write access to user selected files.
And the usere has selected the path you are trying to monitor via FSEvent? Since if he hasn't, you won't be allow to access it and thus also not monitor it. A path can only be monitored as long as you are allowed to access it.
Let me start by saying that I had the PeriodicTask already working a couple of days back, but when I came back to do something else I noticed the PeriodicTask's OnInvoke is not called anymore.
I think I am doing the basics correct: removing existing PeriodTask if found, calling LaunchForTest only in debug build and I've checked that the ScheduledAgent is referenced properly in the project and the WMAppManifest.xml.
This is how I setup the PeriodicTask:
try
{
PeriodicTask backgroundTask = null;
backgroundTask = ScheduledActionService.Find(BGTASK_NEW_EPISODES) as PeriodicTask;
if (backgroundTask != null)
{
ScheduledActionService.Remove(backgroundTask.Name);
}
// Start our background agent.
backgroundTask = new PeriodicTask(BGTASK_NEW_EPISODES);
backgroundTask.Description = "Foobar";
ScheduledActionService.Add(backgroundTask);
#if DEBUG
ScheduledActionService.LaunchForTest(BGTASK_NEW_EPISODES, TimeSpan.FromSeconds(5));
#endif
}
catch (InvalidOperationException e)
{
if (e.Message.Contains("BNS Error: The action is disabled"))
{
App.showNotificationToast("Background tasks have been disabled from\nsystem settings.");
}
}
catch (Exception) { }
}
Here's my WMAppManifest.xml:
<ExtendedTask Name="BackgroundTask">
<BackgroundServiceAgent Specifier="ScheduledTaskAgent" Name="PodcatcherBackgroundService" Source="PodcatcherBackgroundService" Type="PodcatcherBackgroundService.ScheduledAgent" />
</ExtendedTask>
When I install the app for the first time, then the OnInvoke is called. But if I restart the app, it's not called. The same is true for both device and emulator.
I've also verified that the background task is enabled in settings and I have a fully charged battery (device is a WP7 device and it's connected via USB to PC. For the emulator, of course, this doesn't matter).
So what should I check next?
Thanks!
Ok, seems I got it resolved.
In certain cases I forgot to call NotifyComplete() in the background task. In that case Windows Phone seems to just ignore subsequent tries to invoke the background worker.