diff --git a/libgrowl/build.xml b/libgrowl/build.xml new file mode 100644 index 00000000..bf22b556 --- /dev/null +++ b/libgrowl/build.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libgrowl/src/java/info/growl/DummyGrowl.java b/libgrowl/src/java/info/growl/DummyGrowl.java new file mode 100644 index 00000000..135fbe06 --- /dev/null +++ b/libgrowl/src/java/info/growl/DummyGrowl.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2008, Michael Stringer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Growl nor the names of its contributors may be + * used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package info.growl; + +import java.awt.image.RenderedImage; + +/** + * Dummy implementation of the Growl API for situations where the native code + * cannot be loaded (non-Mac systems). + * + * @author Michael Stringer + * @version 0.1 + */ +class DummyGrowl implements Growl { + + /** + * {@inheritDoc} + */ + public void addNotification(String name, boolean enabledByDefault) { + // does nothing + } + + /** + * {@inheritDoc} + */ + public void register() throws GrowlException { + // does nothing + } + + /** + * {@inheritDoc} + */ + public void sendNotification(String name, String title, String body) + throws GrowlException { + // does nothing + } + + /** + * {@inheritDoc} + */ + public void sendNotification(String name, String title, String body, + RenderedImage icon) throws GrowlException { + // does nothing + } + + /** + * {@inheritDoc} + */ + public void setIcon(RenderedImage icon) throws GrowlException { + // does nothing + } + + /** + * {@inheritDoc} + */ + public void sendNotification(String name, String title, String body, + String callbackContext) throws GrowlException { + // does nothing + } + + /** + * {@inheritDoc} + */ + public void sendNotification(String name, String title, String body, + String callbackContext, RenderedImage icon) throws GrowlException { + // does nothing + } + + /** + * {@inheritDoc} + */ + public void addCallbackListener(GrowlCallbackListener listener) { + // does nothing + } +} diff --git a/libgrowl/src/java/info/growl/Growl.java b/libgrowl/src/java/info/growl/Growl.java new file mode 100644 index 00000000..9ea3abed --- /dev/null +++ b/libgrowl/src/java/info/growl/Growl.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2008, Michael Stringer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Growl nor the names of its contributors may be + * used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package info.growl; + +import java.awt.image.RenderedImage; + +/** + * Interface for sending notifications to Growl. + * + * @author Michael Stringer + * @version 0.1 + */ +public interface Growl { + /** + * Registers this Growl object with the Growl service. + * + * @throws GrowlException + * If an error occurs during the registration. + */ + public void register() throws GrowlException; + + /** + * Sets the icon to display for notifications from this Growl. + * + * This must be called before calling {@link #register()}. If the + * icon is changed after {@link #register()} has been called then another + * call to {@link #register()} must be made. + * + * @param icon + * The icon to display. + * @throws GrowlException + * If an error occurs while setting the icon. + */ + public void setIcon(RenderedImage icon) throws GrowlException; + + /** + * Adds a notification type for this Growl. This must + * be called before calling {@link #register()}. If the another + * notification type is added after {@link #register()} has been called then + * another call to {@link #register()} must be made. + * + * @param name + * The name of the notification type. This is what appears in + * the Growl settings. + * @param enabledByDefault + * true if this notification type should be + * enabled by default. This can be overridden by the user in + * the Growl settings. + */ + public void addNotification(String name, boolean enabledByDefault); + + /** + * Adds a click callback listener. + * + * @param listener + * The callback listener to add. + */ + public void addCallbackListener(GrowlCallbackListener listener); + + /** + * Sends a notification to Growl for displaying. This must be called + * after calling {@link #register()}. + * + * @param name + * Name of the notification type that has been registered. + * @param title + * The title of the notification. + * @param body + * The body of the notification. + * @throws GrowlException + * If the notification could not be sent. + */ + public void sendNotification(String name, String title, String body) + throws GrowlException; + + /** + * Sends a notification to Growl for displaying. This must be called + * after calling {@link #register()}. + * + * @param name + * Name of the notification type that has been registered. + * @param title + * The title of the notification. + * @param body + * The body of the notification. + * @param icon + * The icon to display with the notification. + * @throws GrowlException + * If the notification could not be sent. + */ + public void sendNotification(String name, String title, String body, + RenderedImage icon) throws GrowlException; + + /** + * Sends a notification to Growl for displaying. This must be called + * after calling {@link #register()}. + * + * @param name + * Name of the notification type that has been registered. + * @param title + * The title of the notification. + * @param body + * The body of the notification. + * @param callbackContext + * A unique ID that will be sent to any registered + * {@link GrowlCallbackListener}s. If this is + * null then clicks will be ignored. + * @throws GrowlException + * If the notification could not be sent. + */ + public void sendNotification(String name, String title, String body, + String callbackContext) throws GrowlException; + + /** + * Sends a notification to Growl for displaying. This must be called + * after calling {@link #register()}. + * + * @param name + * Name of the notification type that has been registered. + * @param title + * The title of the notification. + * @param body + * The body of the notification. + * @param callbackContext + * A unique ID that will be sent to any registered + * {@link GrowlCallbackListener}s. If this is + * null then clicks will be ignored. + * @param icon + * The icon to display with the notification. + * @throws GrowlException + * If the notification could not be sent. + */ + public void sendNotification(String name, String title, String body, + String callbackContext, RenderedImage icon) throws GrowlException; +} diff --git a/libgrowl/src/java/info/growl/GrowlCallbackListener.java b/libgrowl/src/java/info/growl/GrowlCallbackListener.java new file mode 100644 index 00000000..f294d9d9 --- /dev/null +++ b/libgrowl/src/java/info/growl/GrowlCallbackListener.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2008, Michael Stringer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Growl nor the names of its contributors may be + * used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package info.growl; + +/** + * Callback interface for handling users clicking on notifications. + * + * @author Michael Stringer + * @version 0.1 + */ +public interface GrowlCallbackListener { + + /** + * Signals that the user has clicked on a notification with an identifier of + * clickContext. + * + * @param clickContext + * The ID of the notification clicked. + */ + public void notificationWasClicked(String clickContext); +} diff --git a/libgrowl/src/java/info/growl/GrowlException.java b/libgrowl/src/java/info/growl/GrowlException.java new file mode 100644 index 00000000..d4759966 --- /dev/null +++ b/libgrowl/src/java/info/growl/GrowlException.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2008, Michael Stringer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Growl nor the names of its contributors may be + * used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package info.growl; + +/** + * An exception class for handling errors while sending messages to Growl. + * + * @author Michael Stringer + * @version 0.1 + */ +public class GrowlException extends Exception { + private static final long serialVersionUID = 2642457707267962686L; + + /** + * Creates a new GrowlException. + * + * @param message + * The error message. + */ + public GrowlException(String message) { + super(message); + } + + /** + * Creates a new GrowlException. + * + * @param message + * The error message. + * @param cause + * The underlying cause. + */ + public GrowlException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/libgrowl/src/java/info/growl/GrowlNative.java b/libgrowl/src/java/info/growl/GrowlNative.java new file mode 100644 index 00000000..65a18e84 --- /dev/null +++ b/libgrowl/src/java/info/growl/GrowlNative.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2008, Michael Stringer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Growl nor the names of its contributors may be + * used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package info.growl; + +import java.awt.image.RenderedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.imageio.ImageIO; + +/** + * Growl notification implementation. This uses JNI to send messages to Growl. + * + * @author Michael Stringer + * @version 0.1 + */ +class GrowlNative implements Growl { + private String appName; + private List notifications; + private List callbackListeners; + private byte[] imageData; + + private native void sendNotification(String appName, String name, + String title, String message, String callbackContext, byte[] icon); + + private native void registerApp(String appName, byte[] image, + List notifications); + + /** + * Creates a new GrowlNative instance for the specified + * application name. + * + * @param appName + * The name of the application sending notifications. + */ + GrowlNative(String appName) { + notifications = new ArrayList(); + callbackListeners = new ArrayList(); + this.appName = appName; + } + + void fireCallbacks(String callbackContext) { + for (GrowlCallbackListener listener : callbackListeners) { + listener.notificationWasClicked(callbackContext); + } + } + + /** + * {@inheritDoc} + */ + public void register() throws GrowlException { + registerApp(appName, imageData, notifications); + } + + /** + * {@inheritDoc} + */ + public void addNotification(String name, boolean enabledByDefault) { + notifications.add(new NotificationType(name, enabledByDefault)); + } + + /** + * {@inheritDoc} + */ + public void addCallbackListener(GrowlCallbackListener listener) { + callbackListeners.add(listener); + } + + /** + * {@inheritDoc} + */ + public void setIcon(RenderedImage icon) throws GrowlException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(icon, "png", baos); + + imageData = baos.toByteArray(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * {@inheritDoc} + */ + public void sendNotification(String name, String title, String body) + throws GrowlException { + if (!notifications.contains(new NotificationType(name, false))) { + System.out.println("contains: " + notifications); + throw new GrowlException("Unregistered notification name [" + name + + "]"); + } + sendNotification(appName, name, title, body, null, null); + } + + /** + * {@inheritDoc} + */ + public void sendNotification(String name, String title, String body, + RenderedImage icon) throws GrowlException { + if (!notifications.contains(new NotificationType(name, false))) { + System.out.println("contains: " + notifications); + throw new GrowlException("Unregistered notification name [" + name + + "]"); + } + + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(icon, "png", baos); + + byte[] image = baos.toByteArray(); + + sendNotification(appName, name, title, body, null, image); + } catch (IOException ioe) { + throw new GrowlException("Failed to convert Image", ioe); + } + } + + /** + * {@inheritDoc} + */ + public void sendNotification(String name, String title, String body, + String callbackContext) throws GrowlException { + if (!notifications.contains(new NotificationType(name, false))) { + System.out.println("contains: " + notifications); + throw new GrowlException("Unregistered notification name [" + name + + "]"); + } + + sendNotification(appName, name, title, body, callbackContext, null); + } + + /** + * {@inheritDoc} + */ + public void sendNotification(String name, String title, String body, + String callbackContext, RenderedImage icon) throws GrowlException { + if (!notifications.contains(new NotificationType(name, false))) { + System.out.println("contains: " + notifications); + throw new GrowlException("Unregistered notification name [" + name + + "]"); + } + + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(icon, "png", baos); + + byte[] image = baos.toByteArray(); + + sendNotification(appName, name, title, body, callbackContext, image); + } catch (IOException ioe) { + throw new GrowlException("Failed to convert Image", ioe); + } + } + + private class NotificationType { + private String name; + private boolean enabledByDefault; + + public NotificationType(String name, boolean enabledByDefault) { + this.name = name; + this.enabledByDefault = enabledByDefault; + } + + public String getName() { + return name; + } + + public boolean isEnabledByDefault() { + return enabledByDefault; + } + + public boolean equals(Object other) { + if (!(other instanceof NotificationType)) { + return false; + } + + NotificationType otherType = (NotificationType) other; + + return name.equals(otherType.name); + } + } +} diff --git a/libgrowl/src/java/info/growl/GrowlUtils.java b/libgrowl/src/java/info/growl/GrowlUtils.java new file mode 100644 index 00000000..916d5daa --- /dev/null +++ b/libgrowl/src/java/info/growl/GrowlUtils.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2008, Michael Stringer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Growl nor the names of its contributors may be + * used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package info.growl; + +import java.util.HashMap; +import java.util.Map; + +/** + * Utility class for sending notifications using Growl. + * + * @author Michael Stringer + * @version 0.1 + */ +public final class GrowlUtils { + private static final boolean GROWL_LOADED; + private static Map instances = new HashMap(); + + static { + boolean loaded = false; + try { + System.loadLibrary("growl"); + loaded = true; + } catch (UnsatisfiedLinkError ule) { + System.out.println("Failed to load Growl library"); + } + + GROWL_LOADED = loaded; + } + + /** + * Utility method - should not be instantiated. + */ + private GrowlUtils() { + + } + + /** + * Gets a Growl instance to use for the specified application + * name. Multiple calls to this method will return the same instance. + * + * @param appName + * The name of the application. + * @return The Growl instance to use. + */ + public static Growl getGrowlInstance(String appName) { + Growl instance = instances.get(appName); + + if (instance == null) { + if (GROWL_LOADED) { + instance = new GrowlNative(appName); + } else { + instance = new DummyGrowl(); + } + + instances.put(appName, instance); + } + + return instance; + } + + /** + * Gets whether messages can be sent to Growl. If this returns + * false then {@link #getGrowlInstance(String)} will return a + * dummy object. + * + * @return true if messages can be sent to Growl. + */ + public static boolean isGrowlLoaded() { + return GROWL_LOADED; + } +} diff --git a/libgrowl/src/java/info/growl/test/TestGrowl.java b/libgrowl/src/java/info/growl/test/TestGrowl.java new file mode 100644 index 00000000..e570279a --- /dev/null +++ b/libgrowl/src/java/info/growl/test/TestGrowl.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2008, Michael Stringer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Growl nor the names of its contributors may be + * used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package info.growl.test; + +import info.growl.Growl; +import info.growl.GrowlCallbackListener; +import info.growl.GrowlException; +import info.growl.GrowlUtils; + +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.image.BufferedImage; +import java.io.IOException; + +import javax.imageio.ImageIO; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; + +/** + * Simple test class for Growl. + * + * @author Michael Stringer + * @version 0.1 + */ +public class TestGrowl extends JFrame { + private static final String APP_NAME = "Test Java App"; + private static final String NOTIF_3_CALLBACK = "Notif3"; + + public TestGrowl() { + super("Growl for Java"); + setSize(320, 290); + + buildComponents(); + setVisible(true); + setDefaultCloseOperation(EXIT_ON_CLOSE); + } + + private void buildComponents() { + getContentPane().setLayout(new GridLayout(6, 1)); + + Action action = new AbstractAction("Register") { + public void actionPerformed(ActionEvent ae) { + try { + Growl growl = GrowlUtils.getGrowlInstance(APP_NAME); + + growl.addNotification("Test Notification 1", true); + growl.addNotification("Test Notification 2", false); + growl.addNotification("Test Notification 3", true); + + GrowlCallbackListener listener = new GrowlCallbackListener() { + public void notificationWasClicked( + final String clickContext) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + if (NOTIF_3_CALLBACK.equals(clickContext)) { + JOptionPane + .showMessageDialog( + TestGrowl.this, + "User clicked on 'Test Notification 3'"); + } + } + }); + } + }; + + growl.addCallbackListener(listener); + growl.register(); + } catch (GrowlException ge) { + ge.printStackTrace(); + } + } + }; + + getContentPane().add(new JButton(action)); + + action = new AbstractAction("Send 'Test Notification 1'") { + public void actionPerformed(ActionEvent ae) { + try { + Growl growl = GrowlUtils.getGrowlInstance(APP_NAME); + + growl.sendNotification("Test Notification 1", + "Test Notif 1", "This is a test"); + } catch (GrowlException ge) { + ge.printStackTrace(); + } + } + }; + getContentPane().add(new JButton(action)); + + action = new AbstractAction("Send 'Test Notification 2'") { + public void actionPerformed(ActionEvent ae) { + try { + Growl growl = GrowlUtils.getGrowlInstance(APP_NAME); + BufferedImage image = ImageIO.read(TestGrowl.class + .getResource("/images/duke.gif")); + growl.sendNotification("Test Notification 2", + "Test Notif 2", "This is another test", image); + } catch (GrowlException ge) { + ge.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + }; + getContentPane().add(new JButton(action)); + + action = new AbstractAction("Test Callback 'Notification 3'") { + public void actionPerformed(ActionEvent ae) { + try { + Growl growl = GrowlUtils.getGrowlInstance(APP_NAME); + + growl.sendNotification("Test Notification 3", + "Callback Test", "Click me - I dares you!", NOTIF_3_CALLBACK); + } catch (GrowlException ge) { + ge.printStackTrace(); + } + } + }; + getContentPane().add(new JButton(action)); + + action = new AbstractAction("Reg & Test App 2") { + public void actionPerformed(ActionEvent ae) { + try { + Growl growl = GrowlUtils.getGrowlInstance("Other App"); + + growl.addNotification("A Notification", true); + + BufferedImage image = ImageIO.read(TestGrowl.class + .getResource("/images/duke.gif")); + growl.setIcon(image); + + growl.register(); + growl.sendNotification("A Notification", "Testin", + "Blah de blah blah"); + } catch (GrowlException ge) { + ge.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + }; + getContentPane().add(new JButton(action)); + + action = new AbstractAction("Exit") { + public void actionPerformed(ActionEvent ae) { + System.exit(0); + } + }; + getContentPane().add(new JButton(action)); + } + + public static final void main(String[] args) { + new TestGrowl(); + } +} diff --git a/libgrowl/src/native/GrowlJava.mm b/libgrowl/src/native/GrowlJava.mm new file mode 100644 index 00000000..e78d5899 --- /dev/null +++ b/libgrowl/src/native/GrowlJava.mm @@ -0,0 +1,180 @@ + +#import +#import +#import + +#import "info_growl_GrowlNative.h" +#import "GrowlJavaCallback.h" + +jclass jListClass; +jmethodID jListSize; +jmethodID jListGet; + +jclass gnClass; +jmethodID gnFireCallbacks; + +jclass ntClass; +jmethodID ntGetName; +jmethodID ntIsEnabledByDefault; + +jclass gcClass; +jmethodID gcNotificationWasClicked; + +/* + * Convert a Java String into a Cocoa NSString. + */ +NSString *convertJavaStringToCocoa(JNIEnv *env, jstring javaString) { + NSString *cocoaString; + const char *nativeString; + + nativeString = env->GetStringUTFChars(javaString, JNI_FALSE); + cocoaString = [NSString stringWithUTF8String:nativeString]; + + return cocoaString; +} + +/** + * Initialise references to the necessary Java classes & methods. + */ +void initJavaRefs(JNIEnv *env) { + jListClass = env->FindClass("java/util/List"); + jListSize = env->GetMethodID(jListClass, "size", "()I"); + jListGet = env->GetMethodID(jListClass, "get", "(I)Ljava/lang/Object;"); + + gnClass = env->FindClass("info/growl/GrowlNative"); + gnFireCallbacks = env->GetMethodID(gnClass, "fireCallbacks", "(Ljava/lang/String;)V"); + + ntClass = env->FindClass("info/growl/GrowlNative$NotificationType"); + ntGetName = env->GetMethodID(ntClass, "getName", "()Ljava/lang/String;"); + ntIsEnabledByDefault = env->GetMethodID(ntClass, "isEnabledByDefault", "()Z"); + + gcClass = env->FindClass("info/growl/GrowlCallbackListener"); + gcNotificationWasClicked = env->GetMethodID(gcClass, "notificationWasClicked", "(Ljava/lang/String;)V"); + + env->ExceptionDescribe(); +} + +/* + * Class: info_growl_GrowlNative + * Method: sendNotification + * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[B)V + */ +JNIEXPORT void JNICALL Java_info_growl_GrowlNative_sendNotification +(JNIEnv *env, jobject, jstring jAppName, jstring jNotifName, jstring jTitle, jstring jBody, + jstring jCallbackId, jbyteArray jImage) { + NSAutoreleasePool *releasePool = [[NSAutoreleasePool alloc] init]; + NSString *appName = convertJavaStringToCocoa(env, jAppName); + NSString *notifName = convertJavaStringToCocoa(env, jNotifName); + NSString *title = convertJavaStringToCocoa(env, jTitle); + NSString *body = convertJavaStringToCocoa(env, jBody); + NSDistributedNotificationCenter *notifCenter = [NSDistributedNotificationCenter defaultCenter]; + NSMutableDictionary *notifDictionary = [[NSMutableDictionary alloc] init]; + + // initialise Java classes/methods + initJavaRefs(env); + + // build the dictionary for the notification + [notifDictionary setObject:appName forKey:@"ApplicationName"]; + + [notifDictionary setObject:appName forKey:@"ApplicationName"]; + [notifDictionary setObject:notifName forKey:@"NotificationName"]; + [notifDictionary setObject:title forKey:@"NotificationTitle"]; + [notifDictionary setObject:body forKey:@"NotificationDescription"]; + + if (jCallbackId != NULL) { + // notification has a callback - register it + NSString *callbackId = convertJavaStringToCocoa(env, jCallbackId); + [notifDictionary setObject:callbackId forKey:@"NotificationClickContext"]; + } + + if (jImage != NULL) { + // notification has a custom icon - add it + jbyte *nativeImageData = env->GetByteArrayElements(jImage, NULL); + NSData *imageData = [NSData dataWithBytes:nativeImageData length:env->GetArrayLength(jImage)]; + env->ReleaseByteArrayElements(jImage, nativeImageData, JNI_ABORT); + + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + [notifDictionary setObject:[image TIFFRepresentation] forKey:@"NotificationIcon"]; + } + + // send the notification + [notifCenter postNotificationName:@"GrowlNotification" + object:nil + userInfo:notifDictionary + deliverImmediately:true]; + + // clean up + [releasePool release]; +} + +/* + * Class: info_growl_GrowlNative + * Method: registerApp + * Signature: (Ljava/lang/String;[BLjava/util/List;)V + */ +JNIEXPORT void JNICALL Java_info_growl_GrowlNative_registerApp +(JNIEnv *env, jobject jThis, jstring jAppName, jbyteArray jImage, jobject jNotifList) { + NSAutoreleasePool *releasePool = [[NSAutoreleasePool alloc] init]; + NSDistributedNotificationCenter *notifCenter = [NSDistributedNotificationCenter defaultCenter]; + NSMutableDictionary *regDictionary = [[NSMutableDictionary alloc] init]; + NSString *appName = convertJavaStringToCocoa(env, jAppName); + NSMutableArray *notifNames = [[NSMutableArray alloc] init]; + NSMutableArray *defaultNotifs = [[NSMutableArray alloc] init]; + + // initialise Java classes/methods + initJavaRefs(env); + + // build the dictionary for the registration + [regDictionary setObject:appName forKey:@"ApplicationName"]; + + // loop through the notifications and add them to the list + int notifSize = env->CallIntMethod(jNotifList, jListSize); + for (int i = 0; i < notifSize; i++) { + jobject notifType = env->CallObjectMethod(jNotifList, jListGet, i); + jstring jNotifName = (jstring)env->CallObjectMethod(notifType, ntGetName); + NSString *notifName = convertJavaStringToCocoa(env, jNotifName); + + [notifNames addObject:notifName]; + + if (env->CallBooleanMethod(notifType, ntIsEnabledByDefault)) { + // add to default list + [defaultNotifs addObject:notifName]; + } + } + + [regDictionary setObject:notifNames forKey:@"AllNotifications"]; + [regDictionary setObject:defaultNotifs forKey:@"DefaultNotifications"]; + + // register the click callback + jobject callbackRef = env->NewGlobalRef(jThis); + GrowlJavaCallback *callback = [[GrowlJavaCallback alloc] initWithCallback:callbackRef]; + + NSString *growlNotifName = [[NSString alloc] initWithFormat:@"%@%@", + appName, @"GrowlClicked!"]; + + [notifCenter addObserver:callback + selector:@selector(growlNotificationWasClicked:) + name:growlNotifName + object:nil]; + + if (jImage != NULL) { + // set the image for this application's registration + jbyte *nativeImageData = env->GetByteArrayElements(jImage, NULL); + NSData *imageData = [NSData dataWithBytes:nativeImageData length:env->GetArrayLength(jImage)]; + env->ReleaseByteArrayElements(jImage, nativeImageData, JNI_ABORT); + + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + [regDictionary setObject:[image TIFFRepresentation] forKey:@"ApplicationIcon"]; + } + + // send the registration + [notifCenter postNotificationName:@"GrowlApplicationRegistrationNotification" + object:nil + userInfo:regDictionary + deliverImmediately:true]; + + // clean up + [releasePool release]; +} diff --git a/libgrowl/src/native/GrowlJavaCallback.h b/libgrowl/src/native/GrowlJavaCallback.h new file mode 100644 index 00000000..37980499 --- /dev/null +++ b/libgrowl/src/native/GrowlJavaCallback.h @@ -0,0 +1,14 @@ + +#import + +@interface GrowlJavaCallback : NSObject { + jobject _callbackObject; + jclass _callbackClass; + jmethodID _callbackMethod; +} + +- (id) initWithCallback : (jobject) callback; + +- (void) growlNotificationWasClicked : (NSNotification *)notification; + +@end \ No newline at end of file diff --git a/libgrowl/src/native/GrowlJavaCallback.mm b/libgrowl/src/native/GrowlJavaCallback.mm new file mode 100644 index 00000000..d3b14fa0 --- /dev/null +++ b/libgrowl/src/native/GrowlJavaCallback.mm @@ -0,0 +1,51 @@ + +#import +#import +#import + +#import "GrowlJavaCallback.h" + +static JavaVM *cachedJVM = NULL; +@implementation GrowlJavaCallback + +- (id) initWithCallback : (jobject) callback { + self = [super init]; + + _callbackObject = callback; + + return self; +} + +- (void) growlNotificationWasClicked : (NSNotification *)notification { + // get the callback id + NSDictionary *callbackDictionary = (NSDictionary *)[notification userInfo]; + NSString *callbackId = (NSString *)[callbackDictionary objectForKey:@"ClickedContext"]; + const char *nativeString; + jstring convertedString; + + if (cachedJVM == NULL) { + jsize jvmCount; + jint jvmError = JNI_GetCreatedJavaVMs(&cachedJVM, 1, &jvmCount); + + if (jvmError != 0) { + // TODO panic like crazy + NSLog(@"Error error error"); + } + } + + if (cachedJVM != NULL) { + JNIEnv *env = NULL; + cachedJVM->GetEnv((void **)&env, JNI_VERSION_1_2); + + _callbackClass = env->GetObjectClass(_callbackObject); + _callbackMethod = env->GetMethodID(_callbackClass, "fireCallbacks", "(Ljava/lang/String;)V"); + + nativeString = [callbackId UTF8String]; + convertedString = env->NewStringUTF(nativeString); + + // fire off the callback + env->CallVoidMethod(_callbackObject, _callbackMethod, convertedString); + } +} + +@end \ No newline at end of file diff --git a/libgrowl/src/native/Makefile b/libgrowl/src/native/Makefile new file mode 100644 index 00000000..a6d5e9a6 --- /dev/null +++ b/libgrowl/src/native/Makefile @@ -0,0 +1,30 @@ +CXX = g++ +CXXFLAGS = -W -Wall -c -I/System/Library/Frameworks/JavaVM.framework/Headers -I/System/Library/Frameworks/AppKit.framework/Headers -I/System/Library/Frameworks/Foundation.framework/Headers -isysroot /Developer/SDKs/MacOSX10.5.sdk -arch i386 -arch ppc -arch x86_64 -mmacosx-version-min=10.4 +MMFLAGS = -Wno-protocol -Wundeclared-selector +LDFLAGS = -dynamiclib -isysroot /Developer/SDKs/MacOSX10.5.sdk -arch i386 -arch ppc -arch x86_64 -mmacosx-version-min=10.4 +LDSUFFIXES = -framework JavaVM -framework Foundation -framework AppKit +SO_NAME = libgrowl.jnilib + +.SUFFIXES: .o .mm +.mm.o: + $(CXX) $(MMFLAGS) $(CXXFLAGS) $< + +MMSRCS = GrowlJava.mm \ + GrowlJavaCallback.mm + +.PHONY: all clean + +all: $(SO_NAME) + +SO_o = $(MMSRCS:.mm=.o) + +.cpp.o: + $(CXX) $(CXXFLAGS) $(INCLUDES) $(EXTRA_INCLUDES) -o $@ $< + +$(SO_NAME): $(SO_o) + $(CXX) $(LDFLAGS) $(LIBS) $(SO_o) -o $@ $(LDSUFFIXES) + +clean: + rm -f *.jnilib + rm -f *.o + diff --git a/libgrowl/src/resources/images/duke.gif b/libgrowl/src/resources/images/duke.gif new file mode 100644 index 00000000..1e5ac908 Binary files /dev/null and b/libgrowl/src/resources/images/duke.gif differ