【问题标题】:Handle custom URL schemes in an OS X Java application在 OS X Java 应用程序中处理自定义 URL 方案
【发布时间】:2013-12-03 04:28:39
【问题描述】:

我们基于 Java 的应用程序的 Info.plist 包含以下条目:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
<plist version="0.9">
    <dict>
        ...
        <key>CFBundleURLTypes</key>
        <array>
            <dict>
                <key>CFBundleURLName</key>
                <string>myApp handler</string>
                <key>CFBundleURLSchemes</key>
                <array>
                    <string>myapp</string>
                </array>
            </dict>
        </array>
        ...
    </dict>
</plist>

它应该处理像myapp://foobar/bazz 这样的URL。打开应用正常,但是应用应该如何获取点击的URL呢?

【问题讨论】:

    标签: java macos swt


    【解决方案1】:

    对于 Objective C,可以在这里找到答案:When an OS X app is launched by a registered URL scheme, how do you access the full URL?

    Java 的解决方案是将 ObjC 代码重写为纯 C,然后借助 org.eclipse.swt.internal.cocoa.* 下的一组类将其转换为 Java。

    作为 ObjC-to-C 翻译的参考,我们需要 Apple 的Objective-C Runtime Reference

    纯 C 版本

    首先,让我们翻译

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

    进入纯 C。要在纯 C 中调用 ObjC 函数,我们使用 objc_msgSend()。此外,@selector(method_footprint)sel_registerName("method_footprint") 替换,类被objc_getClass() 查找。 idSEL 类型等价于指针(例如 void*)或全尺寸 int(即与 void* 大小相同)。

    结果:

    // 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);
    

    如您所见,我们在这里有两个子例程调用:第一个调用NSAppleEventManager 类的sharedAppleEventManager 消息,从该类中检索一个单例对象。第二个调用是向该对象发送 setEventHandler... 消息,传递 4 个参数(目标对象、目标消息和两个事件说明符)。

    回调函数的声明,原文:

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

    在纯 C 中看起来像这样:

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

    这意味着当函数被调用时,它不仅被发送它的对象引用(id),而且它的方法足迹(选择器)。

    在 ObjC 中获取 URL 的回调代码如下所示:

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

    在纯 C 中:

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

    仍然缺少一个部分:

    targetObject 需要在调用上面的代码之前进行初始化,并且需要在该目标对象中创建与handleAppleEvent:withReplyEvent: 足迹匹配的方法,然后链接到我们的普通 C 事件处理程序 (handleAppleEvent())。

    这意味着我们必须创建一个Objective C类,为其添加一个方法,然后为其创建一个对象实例:

    // 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)
    

    纯 C 版本到此结束。

    (注意:以上代码是未经测试编写的,因此可能存在错误。但是,下面的Java代码已经过测试。)

    Java 版本

    从普通 C 语言到 Java 的翻译现在相当简单。

    前面提到的 ObjC Runtime 函数都可以从 org.eclipse.swt.internal.cocoa.OS 获得。

    首先,一些预设:

    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; // '----'
    

    回调函数:

    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;
    }
    

    以及注册回调函数的代码:

    // 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);
    

    剩下要做的就是从这段代码构建一个应用程序包,然后将 CFBundleURLTypes 条目添加到它的 Info.plist。

    完整的示例源文件可以在这里下载:http://files.tempel.org/Various/ObjectiveC-bridging.java.zip

    【讨论】:

    • 'SEL' 并不是真正的消息签名,它是方法名称。签名将包括类型(例如 NSMethodSignature)。
    • 喂。您已经完成了一些巧妙的 Objective-C 运行时工作,但这不是处理 Apple 事件的正确 C(或 Java)方式。处理 Apple 事件的 C 方法是使用 Carbon Apple 事件管理器(未弃用)。查看 Apple 事件管理器中的 AEInstallEventHandler 函数:developer.apple.com/legacy/library/documentation/Carbon/…
    • 正确的 Java 方式可能是使用 Apple 的“com.apple.eawt”Java 类。见这篇文章:developer.apple.com/library/mac/documentation/java/conceptual/…
    • @AriX 你在这里做了很多假设。你自己试过吗?当然 eawt 包在这种情况下没有帮助。
    • 不,我自己没有在 Java 中实现这个,但我认为我在这里没有做出任何不正确的假设。从我的第一个链接中使用 Carbon Apple 事件管理器(C 调用)会比调用 Objective-C 更清洁,后者是一个聪明但笨拙的解决方案。我无法为 eawt 提供担保,因为我从未使用过它,但我可以为 Apple Event Manager 提供担保。
    【解决方案2】:

    如果有人想要使用 com.apple.eawt 的版本。* 这也使用反射,因此它将在任何平台(Windows 等)上编译。确保不要在任何非 Apple 系统上调用注册事件处理程序的方法;)

    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();
            }
        }
    }
    

    像这样使用它:

    //if(isOSX){
    OSXAppleEventHelper.setOpenURIAppleEventHandler(new OpenUriAppleEventHandler() {
    
        @Override
        public void handleURI(URI url) {
            /* do something with the url */
        }
    });
    

    【讨论】:

    • 这似乎是一个处理程序,用于在应用程序启动后读取完整的 URL 以供后续点击 URL?还是我误会了?我有一个 JavaFX 应用程序,在将底部代码添加到我的启动类的构造函数中时,我可以在应用程序启动后获取 URL,但我需要 launch 参数。这种方法可以做到吗?
    • StuartQ,是的,这为正在运行的应用程序注册了一个处理程序。此外,它需要在 AWT 初始化之后调用(这似乎注册了它自己的 AppleEvent 处理程序)。我不知道启动参数,但这不是获取它们的方法。如果“启动参数”是指启动应用程序时传递的应用程序参数,例如从命令行,则可以通过传递给应用程序的 main() 方法的 String[] 数组获取这些参数。
    • 我的意思是当应用程序通过单击 URL 启动时,例如myapp://foobar/bazz 开头问题中提到的。我有兴趣获得 foobar 和/或 bazz。在这种情况下,main() 中的 string[] 数组为空。
    • 当然可以,参见上面的“handleURI(URI url)”方法。 “url”参数就是你想要的。它应该包含点击的 url。
    • 不幸的是,“handleURI(URI url)”没有给我我需要的东西。它不会在启动时调用,只有在应用程序启动并运行后单击该链接。到那时,已经太晚了。
    【解决方案3】:

    使用 Java 9,这非常容易,不再需要 Apple 的 EAWT 类或任何 ObjC 黑客技术。

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

    需要捆绑应用,并且必须设置CFBundleURLTypes键。

    <!-- 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>
    

    不幸的是,如果应用程序已经在运行,这只会捕获 URI。如果应用程序是通过打开 URI 启动的,则不会传递事件(参见 cmets on ed22's answer)。

    【讨论】:

    • 对我来说,在带有 Java 15 的 MacOS 11.2.1 上,当单击具有自定义协议的网页上的链接时,会弹出一条消息,要求打开应用程序(设置为“始终允许这种类型的链接在关联的应用程序中打开”。应用程序启动并传递事件。
    猜你喜欢
    • 2017-07-18
    • 2015-02-10
    • 2017-10-23
    • 1970-01-01
    • 2014-04-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多