Cocoa protocol handler using NSAppleEventManager and kInternetEventClass/kAEGetURL - objective-c

This is the Cocoa version of this question:
AEInstallEventHandler handler not being called on startup
Here's my Info.plist protocol registration:
...
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>My Protocol</string>
<key>CFBundleURLIconFile</key>
<string>myicon</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Here's where I set the method to listen for the kInternetEventClass/kAEGetURL event when a browser link is clicked with the link "myapp://unused/?a=123&b=456":
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
[[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:#selector(getURL:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
...
}
Here's the handler method:
- (void)getURL:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)reply
{
[[[event paramDescriptorForKeyword:keyDirectObject] stringValue] writeToFile:#"/testbed/complete_url.txt" atomically:YES];
}
Here's the test web link:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
</head>
<body>
Open My App
</body>
</html>
This all works great if the application is already running.
The handler method is called and the complete url is captured.
However, if the app is not yet running the same link will launch the app, but the handler will not be invoked — which makes sense since the handler had not yet been bound to the event.
Those arguments in the URL are important for our application to coordinate with the webapp. Although the vast majority of the time our application will already be running when this click occurs, it is reasonable to expect that in some cases it will not.
I've tried inspecting the environment and process invocation arguments and I do not see the URL in either of them.
Anyone know how we can capture this URL reliably, even when our application isn't already running when the browser click happens?

Apple’s SimpleScriptingPlugin sample registers the handler in applicationWillFinishLaunching:, which is probably cleaner than using init. (And like mikker said, the handler gets called before applicationDidFinishLaunching:.)

I just faced this exact problem. I don't know if it's the proper solution but you can register the event handler in the app delegate's init-method instead.
// in AppDelegate.m
- (id)init
{
self = [super init];
if (self) {
[[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:#selector(handleURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
}
return self;
}
One thing to note though is that handleURLEvent:withReplyEvent get's called before applicationDidFinishLaunching: if the app is started by an URL Scheme.

Related

iOS 8 requestWhenInUseAuthorization no Popup

I tried to make my AppProject iOS 8 ready. I had read a lot about
[_locationManager requestWhenInUseAuthorization];
and the entry in plist
NSLocationWhenInUseUsageDescription
So I changed all the necessary code lines.
It works fine, but now I have copied my project again from my iOS 7 base to include new features. But when I make the changes for the iOS8 Location Privacy the Popup doesn't appear anymore.
My code worked until I copied.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>tolle sache </string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>fapporite.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
and here is my call
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
_UserLocation = [[CLLocation alloc]init];
_locationManager = [[CLLocationManager alloc]init]; // initializing locationManager
_locationManager.delegate = self;
_locationManager.desiredAccuracy = kCLLocationAccuracyBest; // setting the accuracy
[_locationManager requestWhenInUseAuthorization]; // iOS 8 MUST
[_locationManager startUpdatingLocation]; //requesting location updates
NSLog(#"passed initwithcode");
}
return self;
}
How can I fix this?
From the documentation
NSLocationWhenInUseUsageDescription (String - iOS) describes the
reason why the app accesses the user’s location normally while running
in the foreground. Include this key when your app uses location
services to track the user’s current location directly. This key does
not support using location services to monitor regions or monitor the
user’s location using the significant location change service. The
system includes the value of this key in the alert panel displayed to
the user when requesting permission to use location services.
This key is required when you use the requestWhenInUseAuthorization
method of the CLLocationManager class to request authorization for
location services. If the key is not present when you call the
requestWhenInUseAuthorization method without including this key, the
system ignores your request.
This key is supported in iOS 8.0 and later. If your Info.plist file
includes both this key and the NSLocationUsageDescription key, the
system uses this key and ignores the NSLocationUsageDescription key.
Read about it here.
I find that the easiest way to add this key to your info.plist is to right click you info.plist and choose
Open As->Source Code
and then add the following in the end before </dict></plist>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
If you want you can add a text in between <string></string> that describes to the user why you want to use his/hers location. This text will show up under the default text in the alert.
Try writing a NSLocationWhenInUseUsageDescription in Info.plist
iOS 8.3, Xcode 6.3.1, ARC enabled
The question has been resolved, but I have (2) notes to add from my recent involvement with CLLocationManager.
1) You must have the following keys entered into your *.plist file:
Most commonly used keys have generic more descriptive names, such as "Privacy - Location Usage Description", which really is the "NSLocationUsageDescription" key.
To see the "raw" key names go to "*-Info.plist" and right click in the Navigator area where the keys are listed, see below:
And you get the following results:
The three keys that are related to this article are:
2) Make certain that you allocate and initialize your implementation of CLLocationManager before you try to request authorization or update location.
*.h file:
#interface SomeController : UIViewController <CLLocationManagerDelegate>
#property (strong, nonatomic) CLLocationManager *locationManager;
*.m file:
- (IBAction)locationButton:(UIButton *)sender
{
if (self.locationManager == nil)
{
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
}
else
{
nil;
}
if ([self.locationManager respondsToSelector:#selector(requestWhenInUseAuthorization)])
{
[self.locationManager requestWhenInUseAuthorization];
}
else
{
nil;
}
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
[self.locationManager startUpdatingLocation];
}
Hope this saves someone time! Thanks.
Here's a little gotcha. Make sure you're adding the NSLocationAlwaysUsageDescription or NSLocationWhenInUseUsageDescription keys to the main bundle plist and not one of your test target plists!
Had the same problem caused because I was instantiating CLLocationManager in a local var inside a method, solved it making the CLLocationManager a class property.
After a while I found the solution here, but I'm leaving it here since this is the first result in google, hope I save you some time:
requestWhenInUseAuthorization() not Work in iOS 8 With NSLocationWhenInUseUsageDescription Key in Info.plist
Pertaining to the Apple Watch:
You need to put the requestWhenInUseAuthorization key in the iPhone's Info.plist, not the WatchKit App's.
This has bitten me twice now.

NSService not calling its NSMessage

I'm trying to add a Finder service and all looks fine until I want the service to do its job.
This is the method in my AppDelegate.m:
-(void)uploadFromPasteboard:(NSPasteboard *)pboard userData:(NSString *)udata error:(NSString **)err
{
NSString *filename = [pboard stringForType:NSURLPboardType];
dbg(#"file: %#", filename);
}
The plist configuration:
<key>NSServices</key>
<array>
<dict>
<key>NSRequiredContext</key>
<dict/>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>Upload File</string>
</dict>
<key>NSMessage</key>
<string>uploadFromPasteboard</string>
<key>NSPortName</key>
<string>Finder</string>
<key>NSSendTypes</key>
<array>
<string>NSURLPboardType</string>
</array>
<key>NSReturnTypes</key>
<array/>
</dict>
</array>
All seems fine, the service is displayed in the service menu, but when I click it, nothing happens, no logs or anything else, like its not called at all.
Could someone point me whats wrong cos I'm starting to pull my hair hardly :(
Are you setting your service provider instance? Like this (from: Providing a Service):
EncryptoClass* encryptor = [[EncryptoClass alloc] init];
[NSApp setServicesProvider:encryptor];
Merely having this method in your app delegate class is not enough. In the standard application set-up, having this in your app delegate might be sufficient:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[NSApp setServicesProvider: self];
}
Also you're specifying "Finder" for NSPortName. This is probably not correct. From Services Properties (emphasis mine):
NSPortName is the name of the port on which the application should
listen for service requests. Its value depends on how the service
provider application is registered. In most cases, this is the
application name. This property is ignored for Automator workflows
being used as services.
My reading of the documentation is that the application whose name is in NSPortName is the application that will be used to handle the service request. If the name of your app isn't "Finder" (and it shouldn't be, for obvious reasons) then your app will never be called by the service.

WebKit on 10.8: How to programmatically set the value of an input[type=file] field?

Setup:
Mac OS X 10.8.2
Normal Cocoa app which links against OS-provided version of WebKit framework. Specifically, the contents of: /System/Library/Frameworks/WebKit.framework/Versions/A/Resources/version.plist are:
<plist version="1.0">
<dict>
<key>BuildVersion</key>
<string>5</string>
<key>CFBundleShortVersionString</key>
<string>8536</string>
<key>CFBundleVersion</key>
<string>8536.26.14</string>
<key>ProjectName</key>
<string>WebKit</string>
<key>SourceVersion</key>
<string>7536026014000000</string>
</dict>
</plist>
I have a WebKit-based Cocoa app which loads an HTML document containing a normal HTML form in a WebView. The HTML form contains a file chooser input field like this:
<form name="foo">
<input type="file" name="bar">
</form>
I'd like to set the value of this file chooser programmatically (from Objective-C if possible, but I'll do whatever works).
As far as I can tell, this method has never worked:
DOMHTMLInputElement *inputEl = ... // fetch input element
[inputEl setValue:#"some/file.txt"];
I assume there is some security restriction/policy in WebKit which prevents this from working. I assume this is an intentional security feature in WebKit, not a bug.
However, in OS X 10.7 Lion, I was able to work around this restriction with a bit of a hack.
In Lion, you could programmatically click the input element:
#pragma mark -
#pragma mark WebFrameLoadDelegate
- (void)webView:(WebView *)wv didFinishLoadForFrame:(WebFrame *)frame {
if (frame != [wv mainFrame]) return;
DOMAbstractView *win = (id)[frame windowObject];
DOMDocument *doc = [win document];
DOMHTMLFormElement *formEl = (id)[[doc forms] namedItem:#"foo"];
DOMHTMLInputElement *inputEl = (id)[[formEl elements] namedItem:#"bar"];
[inputEl click];
}
Which would produce a call to -[WebUIDelegate webView:runOpenPanelForFileButtonWithResultListener:]. Then, you could implement that delegate method to programmatically set the value of the result listener immediately:
#pragma mark -
#pragma mark WebUIDelegate
- (void)webView:(WebView *)wv runOpenPanelForFileButtonWithResultListener:(id<WebOpenPanelResultListener>)listener {
[listener chooseFilename:#"some/file.txt"];
}
Although this was an ugly hack, it worked perfectly. It had the desired effect of immedately setting the value of the file upload input element. No "open panel" would appear on screen.
I'm afraid WebKit has stopped allowing this in the version which shipped with 10.8. The part that has changed is:
[inputEl click];
This no longer produces a click event on the element in the WebKit shipping with 10.8.
I have tried other methods of clicking which worked on 10.7, but no longer work on 10.8:
DOMUIEvent *evt = (id)[doc createEvent:#"UIEvents"];
[evt initUIEvent:#"click" canBubble:YES cancelable:YES view:win detail:1];
[inputEl dispatchEvent:evt];
These methods worked in 10.7, but none work in 10.8.
So, Is there any way to programmatically set the value of this file chooser in 10.8?
NOTE: I DO NOT want to ship a custom version of WebKit with my app. Other than that, I'm open to any suggestion (ObjC or JS or whatever).
How can you programmatically set the value of a file chooser in WebKit which ships with 10.8?
I have an example test project (reduced test case) available for your convenience here: http://tod.nu/FileUploadTest.zip
This will work if you dispatch the event via the AppKit event system. Something like:
NSView *docView = [[[webView mainFrame] frameView] documentView];
NSRect docFrame = [docView frame];
NSPoint point = [el boundingBox].origin;
point.y = docFrame.size.height - point.y;
NSEvent *evt = [NSEvent mouseEventWithType:NSLeftMouseDown location:point modifierFlags:0 timestamp:[[NSDate date] timeIntervalSinceReferenceDate] windowNumber:[self.webView.window windowNumber] context:0 eventNumber:0 clickCount:1 pressure:0];
[self.webView.window sendEvent:evt];
evt = [NSEvent mouseEventWithType:NSLeftMouseUp location:point modifierFlags:0 timestamp:[[NSDate date] timeIntervalSinceReferenceDate] windowNumber:[self.webView.window windowNumber] context:0 eventNumber:0 clickCount:1 pressure:0];
[self.webView.window sendEvent:evt];
There's probably a better way to do the coordinate system conversion, but that's not really the important part.

Setting Bundle not outputting correct value

Here's the issue, I launch my app from Xcode and it gets up and running then I switch to settings.app and change a toggle from NO to YES and when I switch back to my app, the key outputs NO not YES.
i think i'm running up against the quote below, but not sure how to get around it, if the user launches the app, and goes to settings and changes the toggle, its now out of sync because Settings.app outputs on first launch to NO. Doesn't make sense that a user can't change the setting the first time they switch to settings.app
"For newly installed applications, default preference values from the application’s Settings bundle are not set until the Settings application runs. This means that if the user runs your application before running Settings, the default values specified in your Settings bundle are unavailable."
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSDictionary *appDefaults = [NSDictionary dictionaryWithObject:#"NO" forKey:#"hideActionBar"];
[defaults registerDefaults:appDefaults];
[defaults synchronize];
and then the code i use to check it
- (void)applicationDidBecomeActive:(UIApplication *)application {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL hidden = [defaults boolForKey:#"hideActionBar"];
NSLog(#"%d",hidden);
if (hidden) {
viewController.actionButton.enabled = NO;
} else {
viewController.actionButton.enabled = YES;
}
}
and my settings Root.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Hide Action Bar</string>
<key>Key</key>
<string>hideActionBar</string>
<key>DefaultValue</key>
<false/>
<key>TrueValue</key>
<true/>
<key>FalseValue</key>
<false/>
</dict>
</plist>
For BOOL variables you need not to set any value if want it to be NO. Because when you will try to access it first time it will return you the same. So remove registerDefaults code(all four lines) from your applicationDidFinishLaunchingWithOptions method.
Explanation:
When you change setting from NO to YES and launch your application, in applicationDidFinishLaunchingWithOptions this value is once changed to NO programmatically.
Note: Whenever you use settings bundle in your application, prior to registering a value(object value, because primitive types will return 0) check whether that value is nil(it means still unregistered), if the value for key is nil then only register with initial default values else you will end up in changing the values each time programmatically.
Thanks,

How does one properly lay out a standalone system service in Xcode?

I've recently become interested in writing some system services for OS X but since I have no application to advertise the services under I must resort to writing standalone system services. Apple's documentation on System Services is spartan as it is, but its documentation on standalone services is non-existant.
What I have thus far is an Xcode project built from the bundle package, with the two sources HashifyService.h and HashifyService.m. Here is the test code I have:
- (void) doServiceWork:(NSPasteboard *)pboard
userData:(NSString *)userData
error:(NSString **)error {
NSLog(#"Actually in the service now");
NSString *pboardString;
NSArray *types;
NSLog(#"do test magic service! (pboard: %#, types: %#)", pboard, [pboard types]);
NSString* outputString = #"It Worked";
types = [NSArray arrayWithObject:NSStringPboardType];
[pboard declareTypes:types owner:nil];
[pboard setString:outputString forType:NSStringPboardType];
[outputString release];
return;
}
and this is the NSServices entry in my Info.plist:
<dict>
<key>NSMenuItem</key>
<dict>
<key>Menu item title</key>
<string>HashifyTest</string>
</dict>
<key>NSMessage</key>
<string>doServiceWork</string>
<key>NSPortName</key>
<string>HashifyService</string>
<key>NSReturnTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
<key>NSSendTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
</dict>
I then build the service bundle and place it in ~/Library/Services/ where it is appropriately detected and I am given the option to use the service. Upon activating the service, however, an error occurs and is logged to Console:
..../Hashify.service/Contents/MacOS/Hashify: cannot execute binary file
What am I doing wrong?
You need a main() function. That should register the service using NSRegisterServicesProvider() and enter the run loop. That is in the documentation.