Fetch preferences / permissions for NSUserNotificationCenter - objective-c

I'm currently trying to set up a notification service using the old NSUserNotification API on macOSX (not iOS) in objective c++.
The catch is that I would need to know the system preferences associated with notifications that the user set (and preferably during the rest of runtime too) to determine if my notifications are actually seen. The application is meant to get the users attention in some way to guarantee things like reminders don't go unnoticed, so the thing I want to avoid is a notification delivered that silently doesn't produce any sound nor image on the screen whatsoever without the application knowing. If the notification is not allowed the application should handle it in some other way.
I am working on a macOS 10.14 Mojave machine. My overall code with NSUserNotificationCenter works fine with a custom Delegate that implements shouldDeliver, didActivate etc. Its just that I am currently assuming the user didn't press the "frick off" button on my notifications.
Unfortunately, NSUserNotificationCenter and related classes don't appear to feature a function that determines the permissions the way the new API (UNUserNotificationCenter) does. I have been trying to resolve this for a few hours so I was wondering if anyone else has found a solution to this. I can't really use the new API since this is strictly for back-compatibility in favour of the possibly rather prevalent amount of people who don't use mojave yet.
Things I have tried to make it work so far:
Try and see if there is a .plist somewhere where these settings are stored, in the User-specific Library folder as well as the general Library folder.
Try and find said .plist using CFPreferencesCopyKeyList and related methods. Apparently I don't really know the right domain name.
This answer from 2012 which appears to be outdated since I cannot locate the database on my machine anymore. Might be due to me using a mojave machine, or the file has since moved somewhere else.
Try and determine whether we can retro-actively check our permissions by checking a NSUserNotification's "presented" property on didDeliverNotification, shouldPresentNotification in the delegate. Unfortunately as stated in NSUserNotificationCenter itself it still behaves the same whether the preferences allow notifications or not.
Some code I tried includes the following:
CFPreferencesCopyKeyList((CFStringRef) #"com.apple.systemPreferences.plist", kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
CFPreferencesCopyKeyList((CFStringRef) #"com.apple.systemPreferences.plist", kCFPreferencesAnyUser, kCFPreferencesAnyHost);
//Unfortunately returns null with any combination of com.apple.notificationcenter.plist, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost
CFPreferencesCopyKeyList((CFStringRef) #"com.apple.notificationcenter", kCFPreferencesAnyUser, kCFPreferencesAnyHost);

On my system (only macOS High Sierra), it looks like the file you're after is:
~/Library/Preferences/com.apple.ncprefs.plist
That would correspond to a combination of kCFPreferencesCurrentUser, and kCFPreferencesAnyHost. (For the record, kCFPreferencesCurrentUser + kCFPreferencesCurrentHost equates to pref files in the ~/Library/Preferences/ByHost/ folder).
I don't think any settings would be stored in the local domain (by that I mean in the root /Library/Preferences/ folder).
I was able to get a list of apps and settings using the following code:
[[NSUserDefaults standardUserDefaults] addSuiteNamed:#"com.apple.ncprefs"];
NSArray *apps = [[NSUserDefaults standardUserDefaults] objectForKey:#"apps"];
NSLog(#"[%# %#] apps == %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), apps);
NOTE: this will not work if your app is sanboxed, as access to other pref files like that will be denied.

Related

Write SDK version to binary when compiling from command line (macOS)

I am trying to detect whether the system is in dark mode.
I have already tried reading AppleInterfaceStyle from the user defaults i.e.
NSString *interfaceStyle = [[NSUserDefaults standardUserDefaults] stringForKey:#"AppleInterfaceStyle"];
BOOL isDark = [#"dark" caseInsensitiveCompare:interfaceStyle] == NSOrderedSame;
which works most of the time but has issues in Auto mode on Catalina.
Now from what I have read is that the more robust approach is to check the effectiveAppearance of NSApplication which looks like this:
NSApplication *app = [NSApplication sharedApplication];
NSAppearance *appearance = app.effectiveAppearance;
NSAppearanceName appearanceName = [appearance bestMatchFromAppearancesWithNames:#[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]];
BOOL isDark = [appearanceName isEqualToString:NSAppearanceNameDarkAqua];
The problem with this approach is that the application I am writing this for manually sets its appearance property, which prevents the effectiveAppearance from using the system appearance.
I tried settings app.appearance = nil before checking the effectiveAppearance but it didn't help.
Now there also is [NSAppearance currentAppearance] which uses the appearance of the current thread. I'm not quite sure what this value resolves to if the thread hasn't set the value explicitly.
My big problem here is that I have no access to a machine running macOS to check my code, so I would highly appreciate if someone knows what to do here.
Edit: It looks like the issue is that the library isn’t compile against the correct version of the SDK. Or at least that version isn’t written to the library information.
From the documentation:
If you build your app against an earlier SDK but still want to support Dark Mode, include the NSRequiresAquaSystemAppearance key (with a value of NO) in your app's Info.plist file. Do so only if your app's appearance looks correct when running in macOS 10.14 and later with Dark Mode enabled.
I am already specifying the version through -mmacosx-version-min=10.14. From what I have found this issue is basically the same that I have, but I don’t quite understand what the solution is from the commit.
I guess it has something to do with the -isysroot and -platform_version. But I didn’t find any good reference for what they do and how they work.
My updated question would be:
How do -isysroot and -platform_version work and how do I use them to enable SDK specific functionality with my binaries?
The solution is quite simple. When manually compiling from the command line -mmacosx-version-min=10.14 needs to get passed to the compiler and the linker.

Values not always persisted in App group between companion app & app extension

From time to time, but not always (I have had this working for a bit), the app/extension gets in a state where I can't read a flag set in my App Group between my companion app and my app extension. Don't know how it gets in this state or why the values differ, but it's critical to my application these always be in sync.
Companion app viewDidLoad:
NSUserDefaults *myAppSettings = [[NSUserDefaults alloc] initWithSuiteName:#"group.myapp"];
.....
[myAppSettings setBool:true forKey:#"myBool"];
[myAppSettings synchronize];
NSLog([myAppSettings boolForKey:#"myBool"] ? #"Companion app - bool TRUE" : #"Companion app - bool FALSE");
App extension viewDidLoad
NSUserDefaults *myAppSettings = [[NSUserDefaults alloc] initWithSuiteName:#"group.myapp"];
[myAppSettings synchronize];
NSLog([myAppSettings boolForKey:#"myBool"] ? #"App extension app - bool TRUE" : #"App extension - bool FALSE");
Console output
Companion app - bool TRUE
App extension - bool FALSE
I also synchronize before my companion app will enter background. I have my app group set up in the portal etc.
What am I doing wrong?
EDIT
Apparently others having this problem too:
https://devforums.apple.com/message/977151#977151
"I think that this is currently very glitchy.
Sometimes the data sharing works, then a change and all of a sudden the widget can't see the shared data anymore (both on Simulator and device).
Annoying and hope it's a bit more reliable in next beta!"
EDIT 2
Looks like another person has reported this exact issue as well:
"I also noticed the same thing too.This not only happen to the
NSUserDefaults, but also all the files in the container folder. The
keyboard extension suddenly will lose read/write pemission to the
container folder after using the keyboard for a while."
EDIT 3
More evidence: https://devforums.apple.com/message/1028078#1028078
After I upgrade to beta 3, I noticed that sometimes the keyboard
failed to open the database because it failed to access to the DB
file. The keyboard has been able to access to the file before.
EDIT 4
Seems like this could be because the keyboard loses the RequestsOpenAccess flag. But I can't reproduce it, and there's no way for me to tell for sure.
EDIT 5
Seems like others are reporting this in the iOS8 GM build:
This issue still persists for me in the GM. It seems related to a
keyboard crash.. but also there seems to be some contention between
keyboard and containing app in terms of who creates the suite in what
order. I think this problem is on Apple's end. Trust me, I WANT it to
be my fault but I've spent countless hours with trial and error. No
matter what I do in code and verify with NSLog, it will end up in this
state eventually. Hoping someone finds a magic pill. :S
Has anyone solved this yet?
You must request open access in order to access shared NSUserDefaults. It's stated directly in the App Extension Programming guide:
By default, a keyboard has no network access and cannot share a container with its containing app. To enable these things, set the value of the RequestsOpenAccess Boolean key in the Info.plist file to YES.
Be sure you change the RequestsOpenAccess field to YES. You'll find it in keyboard's Info.plist > NSExtension > NSExtensionAttributes > RequestOpenAccess. Then remove the keyboard in Settings, delete the app, run it again, and add the keyboard again. After you add it, tap on the keyboard name and then flip the switch to enable Allow Full Access. You'll need to instruct the users to follow those same steps to grant access (and reassure them you're not evil), otherwise it simply will not work and you'll never get the data that's stored in your shared container. Note that in iOS 8.3+, if the user hasn't enabled full access the keyboard will be able to access the shared container, but writing to it will not save the data, for security and privacy purposes. In 8.2- you can't access that data without open access granted.
I can confirm that the problem is related to RequestsOpenAccess flag.
Assuming that everything done right (NSUserDefaults use initWithSuiteName, all Capabilities for main application and custom keyboard were set, etc.) I have the next steps:
1) Install the main application and a custom keyboard on device
2) Set 'Allow full access' for the custom keyboard to YES
3) Add some items (in my case this is a simple text templates) in the main app
4) Go to keyboard and check that all items, that were added from the main app,
appeared in custom keyboard
5) Go to main app and add a few more items
6) Go to keyboard and now you will see that nothing changed
7) Go to settings and switch 'Allow full access' to NO and then to YES
8) Go to custom keyboard again and check that item which were added in step 5 appeared.

Storing data in custom keyboard

I am building a custom keyboard that learns the way you type for smarter auto-correct. In order to learn... I need to be able to STORE data to the users device. I've tried using NSFileManager with NSDocumentsDirectory but nothing is getting saved int he AppExtension. (I tested the code (copy paste) in a regular app (non-app-extension) and it worked). I even enabled "Requests Open Access" in the .plist and re-installed the keyboard... still wouldn't save data.
Is there a way to store data in an app-extension?
Possible solutions I've pondered:
•Maybe creating a contact in the users address book that has my info in it (if app-extensions are allowed to do that), but a user might be suspicious as to why my app is requesting permission to modify their contact's address book).
•Displaying a hidden UIWebView that uses javascript injection to store and read data Safari Javascript Database, but I'm afraid this data might be erased if cache is ever cleared.
edit: now this is no longer working for me on iDevice beta 5 (with requestOpenAccess enabled OR disabled)? But it still works on SIMULATOR beta 5? Hmmm
__ original post below: _
It turns out NSUserDefaults DOES save data, it just does not work on BETA 3 simulator, but did work on BETA 5 iDevice. RequestsOpenAccess does NOT need to be enabled!
NSUserDefaults *defaults = [[NSUserDefaults alloc]initWithSuiteName:#"com.company.keyboard.uniqueCodeHere"];//uniqueCodeHere can be anything... we just have to make sure the SuiteName isn't equal to your info.plist bundle ID so we add a random uniqueCode to the end of this ID.
[defaults setObject:#"myStringData" forKey:#"savedStrings"];
[defaults synchronize];
*Note, SuiteName Can NOT equal bundle ID in info.plist or it doesn't work for some reason...

Couple service related "bugs"

I've written an application that can be invoked as a service (by right-clicking a file in Finder and selecting to open it with my application), but there are a couple of unwanted side-effects when doing this.
Example of service target method:
- (void)doSomething:(NSPasteboard *)pboard userData:(NSString *)userData error:(NSString **)error {
NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
NSLog(#"Selected file(s): %#", files);
[self.anotherWindow makeKeyAndOrderFront:self];
}
1) When the application is launched this way (even if already open in debug mode), I seem unable to access other windows/controls from the doSomething function. The above attempt to show "anotherWindow", for example, produces no errors, but doesn't "do" anything. According to the stack trace, when inspected from -doSomething, all gui components have values 0x0000000000000000 - yet the application is displayed and fully functional. It's only from -doSomething that I cannot reach them. "self" also has a different value when inspected from -doSomething versus -applicationDidFinishLaunching. I'm not sure how or why -doSomething is acquiring a different self/AppDelegate with uninitialized components. Seemingly fixed by [NSApp setServicesProvider:self];
2) I am not clear on how the system decides which copy of the application to launch when the service is invoked, but it usually doesn't pick the one I want. I have a copy in /Debug, a copy in /Release, a copy on my desktop... and if I delete one, it opens the file with another one instead (some sort of fallback-chain?). How do I configure the service (in code or thru .plist) to open a specific version/location of this app? But this is a dev machine. If I release a distributable which installs to /Applications, do I ever really need to worry about this?
1) Double-check your XIB to makes sure that you've got everything hooked up correctly and then try launching the app with a breakpoint set at the NSLog above and verify that self.anotherWindow points at what you want. If, for some reason, the breakpoint isn't firing, trying adding an:
NSLog( #"Window: %#", self.anotherWindow);
To make sure everything is initialized and hooked up
2) The system uses Launch Services to determine which version of the application to launch. Often it is the version most recently added to the system (which will cause the Launch Services database to be modified), but it is possible, depending on how your system is configured, that it won't be the version you expect.
You can manually inquire and modify the launch services database using:
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister
(yeah, really long path). If you use the -dump option, that'll give you all of the data in the system (pipe into a file and search through it to get a better idea what's going on). If you search of the bundle id, you'll see all of the entries for the app. Generally, most recent wins, but you can force a reload (instructions below).
If you just want to force a reload based on a a particular binary, use the -f flag and the path to the application:
..../lsregister -f /Applications/Foo.app
You can also use -u to explicitly unregister something.
Hopefully this will give you an idea what's going on here.

How to intercept reading of plist values in Objective-C code?

We're using the new Urban Airship iOS plugin for PhoneGap.
In the plugin's plist file, we're supposed to enter the app-specific keys needed to enable push notifications.
The problem is we have two versions, free and paid, of the same app, but the plist file only accommodates one version.
Essentially, we need to modify the Objective-C code to read different plist values, depending on whether it's the free or premium version.
We currently manage both versions with the same code base and Xcode project. Unless we change the plugin code, it seems like we need to create a new Xcode project, which we don't want to do.
How do we adjust Urban Airship's Objective-C files to read different values from the plsit file?
Sorry to keep you waiting, I wanted to give you a very detailed answer instead of rushing last night :) So here we go.
First in your project we need to add a new target. Go to your project settings and right click your target. Click duplicate.
You'll get a new target probably named Target-copy. You'll also get a new info.plist file just for that target.
Next we're going to edit our Pro version's Built Settings. Scroll or search and find Apple LLVM compiler 4.0 Preprocessing. Add to both your Debug and Release configurations. I normally just go with the simple PRO=1. You also need to add PRO=0 to your lite version or it will be undefined when you try to build that version.
Now lets look at how to add a custom plist like I'm sure you'll need. First create two folders. Its important these are folders not groups. In each folder we can create a plist with the exact same filename.
Since Now you can add something to each of them. I just added a key property and a value pro string / lite string. Finally to the code. In the sample project I made I simple overrode viewDidLoad but obviously this will work anywhere. Since the plists have the same name you can load them with one line of code. They'll never get mixed up because they are only copied to their respective target. If you need to do code level based logic you can use the PRO preprocessor we made.
- (void)viewDidLoad
{
[super viewDidLoad];
// This will load the proper plist automatically.
NSLog(#"Plist Value: %#",[[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"Property List" ofType:#"plist"]] objectForKey:#"property"]);
// Also remember we set up a preprocessor PRO. you can use it as well.
if (PRO) {
NSLog(#"Only Show for Pro");
} else {
NSLog(#"Only Show for Lite");
}
NSLog(#"This will show for both");
}
This is the method I use for all my lite/pro version apps so I can share a common codebase without copying it between projects or other complicated systems. It has worked pretty well for me so far :) Happy Coding!
Source
Figured someone may be able to use the project to look at so here it is on GitHub.