Handle custom URL schemes in an OS X Java application

后端 未结 3 1112
我在风中等你
我在风中等你 2020-12-16 07:21

The Info.plist of our Java-based application contains following entries:




        
相关标签:
3条回答
  • 2020-12-16 07:57

    With Java 9, this is easy quite easy, and no longer requires Apple's EAWT classes or any ObjC hackery.

        Desktop.getDesktop().setOpenURIHandler((event) -> {
            System.out.println("Open URI: " + event.getURI());
            // do something with the URI
        });
    

    The application needs to be bundled, and the CFBundleURLTypes key must be set.

    <!-- Open URIs with scheme example:// -->
    <key>CFBundleURLTypes</key>
    <array>
      <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>example</string>
        </array>
        <key>CFBundleURLName</key>
        <string></string>
      </dict>
    </array>
    

    Unfortunately this only captures the URI if the application is already running. If the application was launched by opening a URI, the event is not delivered (see comments on ed22's answer).

    0 讨论(0)
  • 2020-12-16 08:09

    For Objective C the answer can be found here: When an OS X app is launched by a registered URL scheme, how do you access the full URL?

    The solution for Java is to rewrite the ObjC code into plain C, then translate that into Java, with the help of a set of classes under org.eclipse.swt.internal.cocoa.*.

    As a reference for the ObjC-to-C translation, we need Apple's Objective-C Runtime Reference.

    Plain C version

    First, let's translate

    [[NSAppleEventManager sharedAppleEventManager]
        setEventHandler:targetObject
            andSelector:@selector(handleAppleEvent:withReplyEvent:)
          forEventClass:kInternetEventClass
             andEventID:kAEGetURL];
    

    into plain C. To invoke a ObjC function in plain C, we use objc_msgSend(). Furthermore, @selector(method_footprint) is substituted by sel_registerName("method_footprint"), and classes are looked up with objc_getClass(). The types id and SEL are equivalent to a pointer (such as void*) or a full-size int (i.e. same size as a void*).

    The result:

    // id mgr = [NSAppleEventManager sharedAppleEventManager]
    SEL sel_ sharedAppleEventManager = sel_registerName("sharedAppleEventManager");
    id mgr = objc_msgSend (objc_getClass("NSAppleEventManager"), sharedAppleEventManager);
    
    // and the rest
    SEL sel_setEventHandler = sel_registerName("setEventHandler:andSelector:forEventClass:andEventID:");
    SEL sel_handleAppleEvent = sel_registerName("handleAppleEvent:withReplyEvent:");
    objc_msgSend (mgr, sel_setEventHandler, targetObject, sel_handleAppleEvent, kInternetEventClass, kAEGetURL);
    

    As you can see, we have two subroutine invocations here: The first calls the sharedAppleEventManager message of the NSAppleEventManager class, retrieving a singleton object from that class. The second call is sending the setEventHandler... message to that object, passing 4 arguments (target object, target message, and two event specifiers).

    The callback function's declaration, originally:

    - (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
    

    looks like this in plain C:

    void handleAppleEvent (id self, SEL selector, NSAppleEventDescriptor* event, NSAppleEventDescriptor* replyEvent)
    

    This means that when the function gets called, it gets sent not only its object reference (id) but also its method footprint (selector).

    The callback code looks like this in ObjC to get to the URL:

    NSString url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
    

    And in plain C:

    id desc_id = objc_msgSend (event_id, sel_registerName("paramDescriptorForKeyword:"), '----');
    id url_id = objc_msgSend (desc_id, desc_id, sel_registerName("stringValue"));
    

    One part is still missing:

    targetObject needs to be initialized before invoking the code above, and a method matching the handleAppleEvent:withReplyEvent: footprint needs to be created in that target object, and then linked to our plain C event handler (handleAppleEvent()).

    This means that we have to create an Objective C class, add a method to it, and then create an object instance of it:

    // create an NSObject subclass for our target object
    char objcClassName[] = "ObjCAppleEventHandler";
    id objcClass = objc_allocateClassPair (objc_getClass("NSObject"), objcClassName);
    
    // add the callback method to the class
    SEL sel_handleAppleEvent = sel_registerName("handleAppleEvent:withReplyEvent:");
    class_addMethod (objcClass, sel_handleAppleEvent, handleAppleEvent, "i@:@@");
    
    // register the class
    objc_registerClassPair (objcClass)
    
    // create an object instance
    id targetObject = class_createInstance (objcClass, 0);
    
    // ... here follows the above code with the setEventHandler invocation
    // (note: `SEL sel_handleAppleEvent` appears twice - the 2nd one can be removed)
    

    This concludes the plain C version.

    (Note: The above code was written without testing it, so it may contain errors. The Java code below, however, has been tested.)

    Java version

    Translation from Plain C to Java is now fairly straight-forward.

    The aforementioned ObjC Runtime functions are all available from org.eclipse.swt.internal.cocoa.OS.

    First, some presets:

    static final long class_NSAppleEventManager = OS.objc_getClass("NSAppleEventManager");
    static final long sel_sharedAppleEventManager = OS.sel_registerName("sharedAppleEventManager");
    static final long sel_setEventHandler = OS.sel_registerName("setEventHandler:andSelector:forEventClass:andEventID:");
    static final long sel_handleAppleEvent = OS.sel_registerName("handleAppleEvent:withReplyEvent:");
    static final long sel_paramDescriptorForKeyword = OS.sel_registerName("paramDescriptorForKeyword:");
    static final long sel_stringValue = OS.sel_registerName("stringValue");
    
    static final long kInternetEventClass = 0x4755524C; // 'GURL'
    static final long kAEGetURL = 0x4755524C; // 'GURL'
    static final long kCoreEventClass = 0x61657674; // 'aevt'
    static final long kAEOpenApplication = 0x6F617070; // 'oapp'
    static final long kAEReopenApplication = 0x72617070; // 'rapp'
    static final long keyDirectObject = 0x2d2d2d2d; // '----'
    

    The callback function:

    static long handleAppleEvent (long id, long sel, long event_id, long reply_id) {
        // This is a handler for AppleEvents that are registered with [NSAppleEventManager setEventHandler:...]
        // It matches this selector (footprint):
        //   - (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)reply
    
        // Invoke [[event paramDescriptorForKeyword:keyDirectObject] stringValue] to get the direct object containing the URL
        long direct_desc_id = OS.objc_msgSend (event_id, sel_paramDescriptorForKeyword, keyDirectObject);
        long direct_str_id = OS.objc_msgSend (direct_desc_id, sel_stringValue);
        NSString nsStr = new NSString (direct_str_id);
        String str = nsStr.getString();
        // now 'str' contains the URL
    
        System.out.println ("handleAppleEvent invoked -- argument: "+url);
        return 0;
    }
    

    And the code to register the callback function:

    // Get access to a callback function for receiving the sel_handleAppleEvent message
    aeCallback = new Callback(Main.class, "handleAppleEvent", 4);
    long aeProc = aeCallback.getAddress();
    
    // Create a ObjC class that provides a method with the sel_handleAppleEvent footprint
    String objcClassName = "ObjCAppleEventHandler";
    long objcClass = OS.objc_allocateClassPair(OS.class_NSObject, objcClassName, 0);
    OS.class_addMethod(objcClass, sel_handleAppleEvent, aeProc, "i@:@@");
    OS.objc_registerClassPair(objcClass);
    long objcHandlerInstance = OS.class_createInstance (objcClass, 0);
    
    // Invoke [[NSAppleEventManager sharedAppleEventManager] setEventHandler:objcHandlerInstance andSelector:sel_handleAppleEvent forEventClass:kInternetEventClass andEventID:kAEGetURL]
    long sharedAppleEventMgr = OS.objc_msgSend (class_NSAppleEventManager, sel_sharedAppleEventManager);
    OS.objc_msgSend (sharedAppleEventMgr, sel_setEventHandler, objcHandlerInstance, sel_handleAppleEvent, kInternetEventClass, kAEGetURL);
    

    What's left to do is to build an app bundle from this code and then add the CFBundleURLTypes entries to its Info.plist.

    A complete sample source file can be downloaded here: http://files.tempel.org/Various/ObjectiveC-bridging.java.zip

    0 讨论(0)
  • 2020-12-16 08:09

    In case anyone wanted a version using com.apple.eawt.* This also uses reflection, so it will compile on any platform (Windows etc.). Make sure not to call the method registering the event handler on any non-Apple system ;)

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.net.URI;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    
    interface OpenUriAppleEventHandler {
        public void handleURI(URI uri);
    }
    
    class OpenURIEventInvocationHandler implements InvocationHandler {
    
        private OpenUriAppleEventHandler urlHandler;
    
        public OpenURIEventInvocationHandler(OpenUriAppleEventHandler urlHandler) {
            this.urlHandler = urlHandler;
        }
    
        @SuppressWarnings({ "rawtypes", "unchecked"})
        public Object invoke(Object proxy, Method method, Object[] args) {
            if (method.getName().equals("openURI")) {
                try {
                    Class openURIEventClass = Class.forName("com.apple.eawt.AppEvent$OpenURIEvent");
                    Method getURLMethod = openURIEventClass.getMethod("getURI");
                    //arg[0] should be an instance of OpenURIEvent
                    URI uri =  (URI)getURLMethod.invoke(args[0]);
                    urlHandler.handleURI(uri);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            return null;
        }
    }
    
    public class OSXAppleEventHelper {
        /**
         * Call only on OS X
         */
        @SuppressWarnings({ "unchecked", "rawtypes" })
        public static void setOpenURIAppleEventHandler(OpenUriAppleEventHandler urlHandler) {
            try {
                Class applicationClass = Class.forName("com.apple.eawt.Application");
                Method getApplicationMethod = applicationClass.getDeclaredMethod("getApplication", (Class[])null);
                Object application = getApplicationMethod.invoke(null, (Object[])null);
    
                Class openURIHandlerClass = Class.forName("com.apple.eawt.OpenURIHandler", false, applicationClass.getClassLoader());
                Method setOpenURIHandlerMethod = applicationClass.getMethod("setOpenURIHandler", openURIHandlerClass);
    
                OpenURIEventInvocationHandler handler = new OpenURIEventInvocationHandler(urlHandler);
                Object openURIEvent = Proxy.newProxyInstance(openURIHandlerClass.getClassLoader(), new Class[] { openURIHandlerClass }, handler);
                setOpenURIHandlerMethod.invoke(application, openURIEvent);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
    

    Use it like this:

    //if(isOSX){
    OSXAppleEventHelper.setOpenURIAppleEventHandler(new OpenUriAppleEventHandler() {
    
        @Override
        public void handleURI(URI url) {
            /* do something with the url */
        }
    });
    
    0 讨论(0)
提交回复
热议问题