Tutorial: Using the Zipwhip API in Java to Popup Your Text Messages in a Bubble Window
July 17, 2011 Leave a comment
I wrote a couple of posts about sending and receiving text messages via the Zipwhip API. Those posts culminated in console output showing you the results of sending or receiving. In this post I’m going to take things further and actually have a slick looking bubble popup on your desktop showing you your text message as it comes in to the Zipwhip Cloud in real-time. The results will look something like below.
Watch the video below to check out how cool the bubble looks as it fades in and out.
We are going to create a new Eclipse project from scratch. Let’s call it ZipwhipBubble. Go into Eclipse and start a new project.
Call the project ZipwhipBubble.
The source tab can be left with the defaults.
On the Libraries tab make sure to add the 3 JARs that we used in the previous posts including Log4j, SLF4j, and of course the most important library—the Zipwhip API JAR. The easiest way to do this is to just download the zip file below of this entire project.
ZipwhipBubble.zip 1.1 MB
The contents of the zip file are as follows:
After having downloaded the above zip file, you can add the libraries from the JARs folder into the Libraries tab. Your tab should look like the screenshot below.
Click Finish. You should now have a new project like the window below.
You need to add your first class to the project.
Give the class the name “Main” and give it a package like “com.yourcompany.zw”. Of course set yourcompany to your company.
You will see a code window that’s mostly empty like below.
Now paste the following code into your window so you get a full class ready to go without you having to do any of the hard work because I did it all for you. You can also download the full Zip file of this project from the top of this posting that contains all of the JARs, source code, and images for the project.
Main.java 4.8 KB
1: package com.yourcompany.zw;
2:
3: import org.apache.log4j.BasicConfigurator;
4: import org.apache.log4j.Level;
5: import org.apache.log4j.Logger;
6:
7: import com.zipwhip.signals.dto.Message;
8: import com.zipwhip.api.DefaultZipwhipSubscriptionClient;
9: import com.zipwhip.api.HttpConnection;
10: import com.zipwhip.api.signals.JsonSocketSignalClient;
11: import com.zipwhip.signals.Signal;
12: import com.zipwhip.signals.SignalObserver;
13:
14: public class Main {
15:
16: /**
17: * @param args
18: */
19: public static void main(String[] args) {
20: // Setup logging
21: final Logger log = Logger.getLogger(Main.class);
22: log.setLevel(Level.DEBUG);
23: BasicConfigurator.configure();
24:
25: HttpConnection connection = new HttpConnection();
26: connection.setDebug(true);
27:
28: String mobileNumber = "3135551234";
29: String password = "mypassword";
30:
31: try {
32: // This method will send a login request to the Zipwhip network and
33: // if succcessful you will get a sesionKey set in your connection object
34: // Watch out that you don't run "requestLogin()" too much because if you
35: // create more than 50 sessionKeys within 1 day you will no longer be able
36: // to get a key for 24 hours.
37: connection.requestLogin(mobileNumber, password);
38: //connection.setSessionKey("2ecfd63-4c57-4aa6-64a0-b0f120a1677a:1");
39: connection.apiVersion = "/";
40: log.info("Successfully authenticated. Your sessionKey is:" + connection.getSessionKey());
41:
42: } catch (Exception e) {
43: log.fatal("Failed to authenticate and get sessionKey to Zipwhip network.");
44: log.fatal(e);
45: return;
46: }
47:
48: DefaultZipwhipSubscriptionClient zipwhipSubscrClient;
49: JsonSocketSignalClient signalClient;
50:
51: // This will create a new client object that allows you to perform
52: // other tasks against the Zipwhip network once you have created
53: // an authenticated connection, i.e. have a sessionKey to communicate
54: // to the Zipwhip network over.
55: zipwhipSubscrClient = new DefaultZipwhipSubscriptionClient(connection);
56: log.info("Just created our Default Client. Uses HTTP to connect to Zipwhip network.");
57:
58: // We will create our socket connection as well. You still need an HTTP connection because
59: // the Zipwhip API uses HTTP calls to authenticate the socket connection.
60: signalClient = new JsonSocketSignalClient(zipwhipSubscrClient);
61: log.info("Just created our Socket always-connected client. Uses TCP/IP sockets to connect to Zipwhip network.");
62:
63: signalClient.addSignalObserver(new SignalObserver() {
64: @Override
65: public void notifySignalReceived(Signal signal) {
66: log.debug("Signal received with uri " + signal.uri);
67:
68: switch (SignalUri.toSignalUri(signal.uri)) {
69: case SIGNAL_MESSAGE_RECEIVE:
70: // We got a message. Let's show it.
71: // The Message object is contained in the signal.content object
72: // but you need to cast it.
73: Message msg = (Message)signal.content;
74: showIncomingMessageAlert(msg);
75: break;
76: case SIGNAL_CONVERSATION_CHANGE:
77: // Do nothing for now
78: break;
79: default:
80: // Do nothing if we don't know the signal
81: break;
82: }
83: }
84:
85: @Override
86: public void notifySignalProviderEvent(boolean isConnected, String message, long frameCount) {
87: log.debug("Reporting SignalProvider event: isConnected " + isConnected + ", message: " + message + ", frames: " + frameCount);
88: }
89: });
90:
91: signalClient.connect(connection.getSessionKey());
92:
93: log.info("Socket test will keep running on socket thread. Thanks for using Zipwhip.");
94: }
95:
96: public static void showIncomingMessageAlert(Message message) {
97: Bubble bubble = new Bubble(message.sourceAddress, message.body, "http://cloudtext.letsbobsled.com");
98: }
99: }
100:
101: enum SignalUri
102: {
103: /*
104: * /signal/message/progress
105: * /signal/messageProgress/messageProgress
106: * /signal/message/send
107: * /signal/message/receive
108: * /signal/message/read
109: * /signal/message/delete
110: * /signal/conversation/change
111: */
112: SIGNAL_MESSAGE_RECEIVE,
113: SIGNAL_MESSAGE_PROGRESS,
114: SIGNAL_MESSAGE_READ,
115: SIGNAL_MESSAGE_DELETE,
116: SIGNAL_MESSAGEPROGRESS_MESSAGEPROGRESS,
117: SIGNAL_CONVERSATION_CHANGE,
118: SIGNAL_CONTACT_NEW,
119: SIGNAL_CONTACT_SAVE,
120: SIGNAL_CONTACT_DELETE,
121: NOVALUE;
122:
123: public static SignalUri toSignalUri(String str)
124: {
125: // We are going to use Java's valueOf method, so
126: // we need to cleanup the URI string first
127: // Get rid of first slash
128: String str2 = str.substring(1, str.length());
129: // convert slashes to underscores
130: str2 = str2.replaceAll("/", "_");
131: // go all upper case
132: str2 = str2.toUpperCase();
133:
134: try {
135: return valueOf(str2);
136: }
137: catch (Exception ex) {
138: System.out.println("Found no match for SignalURI:" + str);
139: return NOVALUE;
140: }
141: }
142: }
You will see that once you paste this code in you will have one error. You need to get the Bubble class that I created for this project.
The Bubble class takes care of all of the details of displaying a nice looking bubble on your desktop. All you have to do is create a bubble and pass it the mobile number, the text message, and a redirection URL to actually reply to the message. There are numerous web apps on the Internet that use the Zipwhip cloud so it is up to you to pick the URL that is appropriate.
Let’s go ahead and add our Bubble class to the project. Right click on the “com.yourcompany.zw” package name and choose New –> Class from the menu.
Call the class Bubble.
You will get a nice raw class like below.
Go ahead and paste in all of my hard work from my Bubble class. There’s a good deal of code in this class. The code is below. You can also download the file if you want. It’s below or it’s in the main Zip file linked to earlier in this posting.
Bubble.java 29.4 KB
1: package com.yourcompany.zw;
2:
3: import java.awt.AlphaComposite;
4: import java.awt.BorderLayout;
5: import java.awt.Color;
6: import java.awt.Container;
7: import java.awt.Dimension;
8: import java.awt.Font;
9: import java.awt.GradientPaint;
10: import java.awt.Graphics;
11: import java.awt.Graphics2D;
12: import java.awt.GraphicsConfiguration;
13: import java.awt.Point;
14: import java.awt.Toolkit;
15: import java.awt.Transparency;
16: import java.awt.event.ActionEvent;
17: import java.awt.event.ActionListener;
18: import java.awt.event.MouseAdapter;
19: import java.awt.event.MouseEvent;
20: import java.awt.event.MouseListener;
21: import java.awt.event.MouseMotionAdapter;
22: import java.awt.event.MouseMotionListener;
23: import java.awt.geom.AffineTransform;
24: import java.awt.geom.Rectangle2D;
25: import java.awt.image.BufferedImage;
26: import java.io.File;
27: import java.io.IOException;
28: import java.net.URI;
29: import java.net.URISyntaxException;
30: import java.net.URL;
31: import java.util.ArrayList;
32: import java.util.Arrays;
33: import java.util.HashMap;
34: import java.util.HashSet;
35: import java.util.List;
36: import java.util.Map;
37: import java.util.Set;
38: import java.util.regex.Pattern;
39:
40: import javax.imageio.ImageIO;
41: import javax.swing.ImageIcon;
42: import javax.swing.JButton;
43: import javax.swing.JComponent;
44: import javax.swing.JDialog;
45: import javax.swing.JFrame;
46: import javax.swing.JPanel;
47: import javax.swing.Timer;
48: import javax.swing.UIManager;
49: import javax.swing.text.AbstractDocument;
50:
51: public class Bubble extends JDialog {
52:
53: // this code is static to manage the number of windows that are open
54: protected static int numOpen;
55: protected static int getOpen() { return numOpen; }
56: protected static void windowOpen() { numOpen++; }
57: protected static void windowClose() { numOpen--; }
58:
59: private int X=0;
60: private int Y=0;
61: private String fromMobileNumber;
62: private String message;
63: private String replyUrl;
64: private boolean toFade = true;
65: private float currOpacity;
66: private Timer fadeInTimer;
67: private Timer fadeOutTimer;
68:
69: javax.swing.JTextPane jTextPaneTxtMsg;
70: AbstractDocument doc;
71:
72: public Bubble(String fromMobileNumber, String message, String replyUrl) {
73: this(fromMobileNumber, message, replyUrl, true);
74: }
75:
76: public Bubble(String fromMobileNumber, String message, String replyUrl, boolean toFade) {
77:
78: // initialize properties
79: setFromMobileNumber(fromMobileNumber);
80: setMessage(message);
81: setReplyUrl(replyUrl);
82:
83: // initialize the UI of the bubble
84: init();
85:
86: // set the location of where this bubble will be shown
87: Dimension ourDim = Toolkit.getDefaultToolkit().getScreenSize();
88: this.setLocation(
89: (int)ourDim.getWidth() - this.getWidth() - 10,
90: 0 + ((this.getHeight() + -20) * getOpen()));
91:
92: // increment how many bubbles are open
93: windowOpen();
94:
95: // ok, show it finally
96: this.setVisible(true);
97: }
98:
99: // Getters/setters
100: public String getFromMobileNumber() {
101: return StringUtil.format(fromMobileNumber, "(###) ###-####");
102: }
103:
104: public void setFromMobileNumber(String fromMobileNumber) {
105: this.fromMobileNumber = fromMobileNumber;
106: }
107:
108: public String getMessage() {
109: return message;
110: }
111:
112: public void setMessage(String message) {
113: this.message = message;
114: }
115:
116: public String getReplyUrl() {
117: return replyUrl;
118: }
119:
120: public void setReplyUrl(String replyUrl) {
121: this.replyUrl = replyUrl;
122: }
123:
124: public boolean isToFade() {
125: return toFade;
126: }
127: public void setToFade(boolean toFade) {
128: this.toFade = toFade;
129: }
130:
131: // Initialize the GUI
132: public void init() {
133: try {
134:
135: // When JFrame or JDialog
136: this.setUndecorated(true);
137: this.setModal(false);
138: this.setFocusableWindowState(false);
139: this.setAlwaysOnTop(true);
140:
141: try {
142: UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
143: } catch (Exception evt) {}
144:
145: // Get bg image
146: final BufferedImage backgroundImg = ImageIO.read(getClass().getResource("/resources/riser_shd.png"));
147:
148: // Set icon & title
149: final BufferedImage z = ImageIO.read(getClass().getResource("/resources/z.png"));
150: ((java.awt.Frame)this.getOwner()).setIconImage(z);
151: this.setTitle(this.getFromMobileNumber() + ": " + this.getMessage());
152:
153: // Set layout
154: this.setLayout(new BorderLayout());
155: JPanel mainPanel = new JPanel(new BorderLayout()) {
156:
157: private static final long serialVersionUID = 1L;
158:
159: // The paintComponent override let's us make the entire
160: // window transparent on the desktop so we get a cool effect
161: @Override
162: protected void paintComponent(Graphics g) {
163: Graphics2D g2d = (Graphics2D) g.create();
164:
165: // code from
166: // http://weblogs.java.net/blog/campbell/archive/2006/07/java_2d_tricker.html
167: int width = backgroundImg.getWidth();
168: int height = backgroundImg.getHeight();
169: GraphicsConfiguration gc = g2d.getDeviceConfiguration();
170: BufferedImage img = gc.createCompatibleImage(
171: width,
172: height,
173: Transparency.TRANSLUCENT);
174: Graphics2D g2 = img.createGraphics();
175: g2.setComposite(AlphaComposite.Src);
176: g2.drawImage(backgroundImg, 0, 0, null);
177: g2.dispose();
178: g2d.drawImage(img, 0, 0, this);
179: g2d.dispose();
180: }
181: };
182:
183: // Setup the window
184: final JDialog wnd = this;
185:
186: // This is the phone number and textarea message panel
187: javax.swing.JPanel jPanelAll = new javax.swing.JPanel();
188: javax.swing.JLabel jLabelMobileNum = new javax.swing.JLabel();
189: this.jTextPaneTxtMsg = new javax.swing.JTextPane();
190: JButton jButtonClose = new javax.swing.JButton();
191: JButton jButtonReply = new javax.swing.JButton();
192:
193: jPanelAll.setOpaque(false);
194: jPanelAll.setDoubleBuffered(false);
195: jPanelAll.setName("jPanelAll"); // NOI18N
196:
197: jLabelMobileNum.setFont( new Font(null, Font.BOLD, 14));
198: jLabelMobileNum.setForeground(new Color(67, 74, 84));
199: jLabelMobileNum.setText(getFromMobileNumber());
200: jLabelMobileNum.setName("jLabelMobileNum"); // NOI18N
201:
202: jTextPaneTxtMsg.setBorder(null);
203: jTextPaneTxtMsg.setEditable(false);
204: jTextPaneTxtMsg.setFont( new Font(null, Font.PLAIN, 12));
205: jTextPaneTxtMsg.setOpaque(false);
206: jTextPaneTxtMsg.setDoubleBuffered(false);
207: jTextPaneTxtMsg.setText(getMessage());
208: jTextPaneTxtMsg.setName("jTextPane1"); // NOI18N
209: jTextPaneTxtMsg.setCaretPosition(0);
210:
211: javax.swing.JScrollPane jScrollPane1 = new javax.swing.JScrollPane(jTextPaneTxtMsg);
212: jScrollPane1.getViewport().setOpaque(false);
213: jScrollPane1.getViewport().setDoubleBuffered(false);
214:
215: jScrollPane1.setBorder(null);
216: jScrollPane1.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
217: jScrollPane1.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
218: jScrollPane1.setOpaque(false);
219: jScrollPane1.setDoubleBuffered(false);
220: jScrollPane1.setWheelScrollingEnabled(true);
221:
222: BufferedImage riserSpriteBtnImg = ImageIO.read(getClass().getResource("/resources/risergang.png"));
223:
224: // Define the close button
225: jButtonClose.setIcon(new ImageIcon(riserSpriteBtnImg.getSubimage(1,1,14,14)));
226: jButtonClose.setRolloverIcon(new ImageIcon(riserSpriteBtnImg.getSubimage(16,1,14,14)));
227: jButtonClose.setPressedIcon(new ImageIcon(riserSpriteBtnImg.getSubimage(31,1,14,14)));
228: jButtonClose.setBorder(null);
229: jButtonClose.setBorderPainted(false);
230: jButtonClose.setContentAreaFilled(false);
231: jButtonClose.addActionListener(new ActionListener() {
232: @Override
233: public void actionPerformed(ActionEvent e) {
234: wnd.setVisible(false);
235: }
236: });
237:
238: // Define the reply button
239: jButtonReply.setIcon(new ImageIcon(riserSpriteBtnImg.getSubimage(0,16,58,30)));
240: jButtonReply.setRolloverIcon(new ImageIcon(riserSpriteBtnImg.getSubimage(0,47,58,30)));
241: jButtonReply.setPressedIcon(new ImageIcon(riserSpriteBtnImg.getSubimage(0,78,58,30)));
242: jButtonReply.setBorder(null);
243: jButtonReply.setBorderPainted(false);
244: jButtonReply.setContentAreaFilled(false);
245: jButtonReply.addActionListener(new ActionListener() {
246: @Override
247: public void actionPerformed(ActionEvent e) {
248: openUri(getReplyUrl());
249: wnd.setVisible(false);
250: }
251: });
252:
253: // Do the layout positioning using horiz/vert groups
254: javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanelAll);
255: jPanelAll.setLayout(jPanel2Layout);
256:
257: jPanel2Layout.setHorizontalGroup(
258: jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
259: .addGroup(jPanel2Layout.createSequentialGroup()
260: .addGap(32)
261: .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
262: .addComponent(jLabelMobileNum)
263: .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 150, javax.swing.GroupLayout.PREFERRED_SIZE)
264: )
265: .addContainerGap(73, Short.MAX_VALUE))
266: .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
267: .addGroup(jPanel2Layout.createSequentialGroup()
268: .addGap(169)
269: .addComponent(jButtonClose, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE)
270: .addContainerGap(261, Short.MAX_VALUE)))
271: .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
272: .addGroup(jPanel2Layout.createSequentialGroup()
273: .addGap(128)
274: .addComponent(jButtonReply, javax.swing.GroupLayout.PREFERRED_SIZE, 58, javax.swing.GroupLayout.PREFERRED_SIZE)
275: .addContainerGap(251, Short.MAX_VALUE)))
276: );
277:
278: jPanel2Layout.setVerticalGroup(
279: jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
280: .addGroup(jPanel2Layout.createSequentialGroup()
281: .addGap(32)
282: .addComponent(jLabelMobileNum)
283: .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
284: .addComponent(false, jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE)
285: .addContainerGap(64, Short.MAX_VALUE))
286: .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
287: .addGroup(jPanel2Layout.createSequentialGroup()
288: .addGap(28)
289: .addComponent(jButtonClose, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE)
290: .addContainerGap(300, Short.MAX_VALUE)))
291: .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
292: .addGroup(jPanel2Layout.createSequentialGroup()
293: .addGap(168)
294: .addComponent(jButtonReply, javax.swing.GroupLayout.PREFERRED_SIZE, 30, javax.swing.GroupLayout.PREFERRED_SIZE)
295: .addContainerGap(146, Short.MAX_VALUE)))
296: );
297:
298: // End phone/message panel
299:
300: // Setup final main paenl
301: mainPanel.add(jPanelAll, BorderLayout.WEST);
302: mainPanel.setDoubleBuffered(false);
303: mainPanel.setOpaque(true);
304: this.add(mainPanel, BorderLayout.CENTER);
305:
306: this.setAlwaysOnTop(true);
307: this.setSize(216, 234);
308: this.setLocationRelativeTo(null);
309: com.sun.awt.AWTUtilities.setWindowOpaque(this, false);
310:
311: // Watch mouse movements and clicks to allow dragging of window
312: addMouseListener(new MouseAdapter()
313: {
314: public void mousePressed(MouseEvent e)
315: {
316: // Check for double-click
317: if (e.getClickCount() >= 2) {
318: // Open the website to let them reply
319: openUri(getReplyUrl());
320: setVisible(false);
321: } else {
322: // Do drag operation
323: X=e.getX();
324: Y=e.getY();
325: }
326: }
327:
328: });
329:
330: addMouseMotionListener(new MouseMotionAdapter()
331: {
332: public void mouseDragged(MouseEvent e)
333: {
334: setLocation(getLocation().x+(e.getX()-X),getLocation().y+(e.getY()-Y));
335: }
336: });
337:
338: } catch (IOException e) {
339: e.printStackTrace();
340: }
341: }
342:
343: @Override
344: public void setVisible(boolean b) {
345:
346: // Handle fading in or fading out
347:
348: // If setvisible is true
349: if (b) {
350:
351: // See if we need to fade in
352: if (this.toFade) {
353: // mark the popup with 0% opacity
354: this.currOpacity = 0;
355: com.sun.awt.AWTUtilities.setWindowOpacity(this, 0.0f);
356: }
357:
358: super.setVisible(b);
359:
360: final JDialog popupWindow = this;
361:
362: if (this.toFade) {
363: // start fading in
364: this.fadeInTimer = new Timer(50, new ActionListener() {
365: public void actionPerformed(ActionEvent e) {
366: currOpacity += 10;
367: if (currOpacity <= 100) {
368: com.sun.awt.AWTUtilities.setWindowOpacity(popupWindow,
369: currOpacity / 100.0f);
370: // workaround bug 6670649 - should call
371: // popupWindow.repaint() but that will not repaint the
372: // panel
373: popupWindow.getContentPane().repaint();
374: } else {
375: currOpacity = 100;
376: fadeInTimer.stop();
377: }
378: }
379: });
380: this.fadeInTimer.setRepeats(true);
381: this.fadeInTimer.start();
382: }
383:
384: } else {
385:
386: // If setvisible is false
387:
388: // Handle fading out, if they want a fade
389: if (this.toFade) {
390:
391: // cancel fade-in if it's running.
392: if (this.fadeInTimer.isRunning())
393: this.fadeInTimer.stop();
394:
395: final Bubble popupWindow = this;
396:
397: // start fading out
398: this.fadeOutTimer = new Timer(50, new ActionListener() {
399: public void actionPerformed(ActionEvent e) {
400: currOpacity -= 10;
401: if (currOpacity >= 0) {
402: com.sun.awt.AWTUtilities.setWindowOpacity(popupWindow,
403: currOpacity / 100.0f);
404: // workaround bug 6670649 - should call
405: // popupWindow.repaint() but that will not repaint the
406: // panel
407: popupWindow.getContentPane().repaint();
408: } else {
409: fadeOutTimer.stop();
410: popupWindow.setToFade(false);
411: popupWindow.setVisible(false);
412: currOpacity = 0;
413: }
414: }
415: });
416: this.fadeOutTimer.setRepeats(true);
417: this.fadeOutTimer.start();
418:
419: } else {
420:
421: // setVisible is being set to false and we're not in fadeout mode,
422: // so let's let the super handle
423: // it cuz we don't want to interfere if there's no fading going on
424: windowClose();
425: super.setVisible(false);
426: this.removeAll();
427: this.dispose();
428:
429: }
430: }
431: }
432:
433: public void openUri(String url) {
434:
435: if( !java.awt.Desktop.isDesktopSupported() ) {
436:
437: System.err.println( "Desktop is not supported (fatal)" );
438: return;
439: }
440:
441: java.awt.Desktop desktop = java.awt.Desktop.getDesktop();
442:
443: if( !desktop.isSupported( java.awt.Desktop.Action.BROWSE ) ) {
444:
445: System.err.println( "Desktop doesn't support the browse action (fatal)" );
446: return;
447: }
448:
449: try {
450:
451: java.net.URI uri = new java.net.URI( url );
452: desktop.browse( uri );
453: }
454: catch ( Exception e ) {
455:
456: System.err.println( e.getMessage() );
457: }
458:
459: }
460:
461: }
462:
463: class MoveMouseListener implements MouseListener, MouseMotionListener {
464: JComponent target;
465: Point start_drag;
466: Point start_loc;
467:
468: public MoveMouseListener(JComponent target) {
469: this.target = target;
470: }
471:
472: public static JFrame getFrame(Container target) {
473: if (target instanceof JFrame) {
474: return (JFrame) target;
475: }
476: return getFrame(target.getParent());
477: }
478:
479: Point getScreenLocation(MouseEvent e) {
480: Point cursor = e.getPoint();
481: Point target_location = this.target.getLocationOnScreen();
482: return new Point((int) (target_location.getX() + cursor.getX()),
483: (int) (target_location.getY() + cursor.getY()));
484: }
485:
486: public void mouseClicked(MouseEvent e) {
487: }
488:
489: public void mouseEntered(MouseEvent e) {
490: }
491:
492: public void mouseExited(MouseEvent e) {
493: }
494:
495: public void mousePressed(MouseEvent e) {
496: this.start_drag = this.getScreenLocation(e);
497: this.start_loc = this.getFrame(this.target).getLocation();
498: }
499:
500: public void mouseReleased(MouseEvent e) {
501: }
502:
503: public void mouseDragged(MouseEvent e) {
504: Point current = this.getScreenLocation(e);
505: Point offset = new Point((int) current.getX() - (int) start_drag.getX(),
506: (int) current.getY() - (int) start_drag.getY());
507: JFrame frame = this.getFrame(target);
508: Point new_location = new Point(
509: (int) (this.start_loc.getX() + offset.getX()), (int) (this.start_loc
510: .getY() + offset.getY()));
511: frame.setLocation(new_location);
512: }
513:
514: public void mouseMoved(MouseEvent e) {
515: }
516: }
517:
518: class StringUtil {
519:
520: //private static final Logger logger = LoggerFactory.getLogger(StringUtil.class);
521:
522: public static final String EMPTY_STRING = "";
523: public static final int MAX_MOBILE_NUMBER_DIGITS = 16; // Finland being the longest we could find 99500-1-202-444-1212, plus a buffer...
524: public static final List<Character> VALID_NUMBERS;
525: public static final List<Character> VALID_SPECIAL_CHARACTERS;
526:
527: static {
528: VALID_NUMBERS = Arrays.asList(new Character[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' });
529: VALID_SPECIAL_CHARACTERS = Arrays.asList(new Character[]{'!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '`', '[', ']', '\\', '{', '}', '|', '<', '>', '?', ',', '.', '/', ':', ';', '\'', '"', '+', '~', '*', '.'});
530: }
531:
532: /**
533: * Strips all characters that are not numbers (0 - 9) and returns a new
534: * string. Returns and empty string if the mobile number is null or empty.
535: *
536: * @param mobileNumber - mobile number string to parse
537: * @return String - parsed mobile number
538: */
539: public static String safeCleanMobileNumber(String mobileNumber) {
540:
541: //logger.debug("getting clean for " + mobileNumber);
542:
543: if (isNullOrEmpty(mobileNumber)) {
544: //logger.debug("was nullOrEmpty ");
545: return null;
546: }
547:
548: StringBuilder cleanMobileNumber = new StringBuilder();
549: for (int i = 0; i < mobileNumber.length(); i++) {
550: if (VALID_NUMBERS.contains(mobileNumber.charAt(i))) {
551: cleanMobileNumber.append(mobileNumber.charAt(i));
552: }
553: }
554:
555: // remove the first (1) at the beginning of the to match default us
556: // numbers
557: // 10 digits
558: if (cleanMobileNumber.length() > 10 && startsWith(cleanMobileNumber.toString(), "1")) {
559: //logger.debug("clean 1 " + cleanMobileNumber.substring(1));
560: return cleanMobileNumber.substring(1);
561: }
562: if (cleanMobileNumber.length() > 10 && startsWith(cleanMobileNumber.toString(), "+1")) {
563: //logger.debug("clean 2 " + cleanMobileNumber.substring(1));
564: return cleanMobileNumber.substring(2);
565: }
566:
567: //logger.debug("clean " + cleanMobileNumber.toString());
568: return cleanMobileNumber.toString();
569: }
570:
571: /**
572: * Same as safeCleanMobileNumber except for devices
573: * in which case the device number is removed first
574: *
575: * @param mobileNumber - mobile number string to parse
576: * @return String - parsed mobile number
577: */
578: public static String safeCleanMobileNumberRemoveDevice(String mobileNumber) {
579:
580: if (startsWith(mobileNumber, "device:/")) {
581:
582: int index = mobileNumber.lastIndexOf('/');
583:
584: // If the last index of '/' is > than the first
585: if (index > 7) {
586: mobileNumber = mobileNumber.substring(0, index);
587: }
588: }
589:
590: return safeCleanMobileNumber(mobileNumber);
591: }
592:
593: public static boolean isValidEmail(String email){
594: // see http://www.mkyong.com/regular-expressions/how-to-validate-email-address-with-regular-expression/
595: Pattern pattern ;
596: java.util.regex.Matcher matcher;
597: final String EMAIL_PATTERN = "^[\\w\\-]+(\\.[\\w\\-]+)*@([A-Za-z0-9-]+\\.)+[A-Za-z]{2,4}$";
598:
599: pattern = Pattern.compile(EMAIL_PATTERN);
600: matcher = pattern.matcher(email);
601:
602: return matcher.matches();
603: }
604:
605:
606: /**
607: * The length is valid if it is between 3 and 6 or over
608: * 10 and up to and including 20
609: * 3-6 length means short codes
610: * 10 length 000-000-0000
611: * 10+ means international
612: *
613: * @param mobileNumber
614: * @return
615: */
616: public static boolean isValidLengthMobileNumber(String mobileNumber) {
617:
618: if (isNullOrEmpty(mobileNumber)) {
619: return false;
620: }
621:
622: int numberLength = mobileNumber.length();
623:
624: // Wrong if under 3 and between 7 and 9 digits long or longer than 20
625: if (numberLength < 3 || numberLength == 7 || numberLength == 8 || numberLength == 9 || numberLength > MAX_MOBILE_NUMBER_DIGITS) {
626: return false;
627: }
628: // Correct is 3 to 6 for short codes and 10 to 20 for long international
629: // numbers, including a buffer
630: return true;
631: }
632:
633: public static boolean startsWith(String string1, String toFind) {
634: if (string1 == null && toFind == null){
635: // null contains null.
636: return true;
637: } else if (string1 == null){
638: return false;
639: }
640:
641: if (StringUtil.equalsIgnoreCase(string1, toFind)){
642: return true;
643: }
644:
645: return string1.toLowerCase().startsWith(toFind.toLowerCase());
646: }
647:
648: public static boolean endsWith(String source, String toFind) {
649: if (source == null) {
650: return false;
651: }
652:
653: return source.endsWith(toFind);
654: }
655:
656: public static String defaultValue(String string, String defaultValue) {
657: if (StringUtil.isNullOrEmpty(string)){
658: return defaultValue;
659: }
660: return string;
661: }
662:
663: public static final class Schema {
664:
665: public static final String TEL = "tel";
666: }
667:
668: /**
669: * Takes a string representing the display name and returns and array with
670: * first name and last name.
671: *
672: * Returns null if null or empty input
673: *
674: * @param displayName
675: * @return String[]
676: */
677: public static String[] splitDisplayName(String displayName) {
678: if (displayName == null || displayName.length() < 1) {
679: return null;
680: }
681:
682: String[] names = displayName.split(" ");
683:
684: return names;
685: }
686:
687: /**
688: * Takes a string representing a list of mobile numbers as
689: * 5555551212, 5556667878 or
690: * 5555551212; 5556667878
691: *
692: * Returns null if null or empty input
693: *
694: * @param sourceNumber
695: * @return List<String>
696: */
697: public static List<String> splitMobileNumbers(String sourceNumber) {
698:
699: ArrayList<String> result = new ArrayList<String>();
700: int last = 0;
701:
702: for (int i = 0; i < sourceNumber.length(); i++) {
703: if (sourceNumber.charAt(i) == ';' || sourceNumber.charAt(i) == ',') {
704: String number = sourceNumber.substring(last, i).trim();
705: result.add(number);
706: last = i + 1;
707: }
708: }
709: result.add(sourceNumber.substring(last).trim());
710: return result;
711: }
712:
713: /**
714: * Format the mobile number
715: *
716: * @param mobileNumber
717: * @param format
718: * - ###-###-####
719: * @return
720: */
721: public static String format(String mobileNumber, String format) {
722: if (mobileNumber == null || mobileNumber.length() < 1 || format == null || format.length() < 1) {
723: return mobileNumber;
724: }
725:
726: int numberCount = 0;
727: for (int i = 0; i < format.length(); i++) {
728: if (format.charAt(i) == '#') {
729: numberCount++;
730: }
731: }
732: String number = safeCleanMobileNumber(mobileNumber);
733: if (numberCount != number.length()) {
734: return mobileNumber;
735: }
736:
737: List<Character> numberChars = new ArrayList<Character>();
738: char[] chars = new char[format.length()];
739: int count = 0;
740: for (int i = 0; i < format.length(); i++) {
741: if (format.charAt(i) == '#') {
742: numberChars.add(number.charAt(count));
743: chars[i] = number.charAt(count);
744: count++;
745: } else {
746: numberChars.add(format.charAt(i));
747: chars[i] = format.charAt(i);
748: }
749: }
750:
751: return new String(chars);
752: }
753:
754: public static String stringArrayToDelimittedString(String[] arrayString, String delimiter) {
755: return stringArrayToDelimittedString(arrayString, delimiter, null);
756: }
757:
758: public static String stringArrayToDelimittedString(String[] arrayString, String delimiter, String format) {
759:
760: if (arrayString == null || delimiter == null) {
761: return null;
762: }
763:
764: StringBuilder delimittedString = new StringBuilder();
765: if (format == null || format.length() < 1) {
766: for (String number : arrayString) {
767: delimittedString.append(number).append(delimiter);
768: }
769: } else {
770: for (String number : arrayString) {
771: number = StringUtil.format(number, format);
772: delimittedString.append(number).append(delimiter);
773: }
774: }
775:
776: return delimittedString.toString();
777: }
778:
779: /**
780: * Takes a delimited values string and returns is as a set
781: *
782: * @param string
783: * @param delimiter
784: * @return Set<String>
785: */
786: public static Set<String> stringToSet(String string, String delimiter) {
787: if (isNullOrEmpty(string) || isNullOrEmpty(delimiter)) {
788: return null;
789: }
790:
791: String[] toArray = string.split(delimiter);
792: Set<String> toSet = null;
793: if (toArray != null && toArray.length > 0) {
794: toSet = new HashSet<String>();
795: for (String value : toArray) {
796: if (!isNullOrEmpty(value)) toSet.add(value);
797: }
798: }
799: return toSet;
800: }
801:
802: /**
803: * Return true if the string is null. Trims the string and checks if it is
804: * an empty string
805: *
806: * @param string
807: * @return
808: */
809: public static boolean isNullOrEmpty(String string) {
810: return (string == null || string.trim().length() < 1);
811: }
812:
813: public static boolean isNullOrEmpty(String... strings) {
814: for (String string : strings) {
815: if (isNullOrEmpty(string)) {
816: return true;
817: }
818: }
819: return false;
820: }
821:
822: public static boolean exists(String string) {
823: return !isNullOrEmpty(string);
824: }
825:
826: public static boolean equals(String string1, String string2){
827: if (string1 == string2)
828: return true; // covers both null, or both same instance
829: if (string1 == null){
830: return false; // covers 1 null, other not.
831: }
832:
833: return (string1.equals(string2)); // covers equals
834: }
835:
836: public static boolean equalsIgnoreCase(String string, String type) {
837: boolean oneEmpty = isNullOrEmpty(string);
838: boolean otherEmpty = isNullOrEmpty(type);
839: if (oneEmpty && otherEmpty) {
840: return true;
841: }
842: if (oneEmpty || otherEmpty) {
843: return false;
844: }
845:
846: return string.equalsIgnoreCase(type);
847: }
848:
849: public static boolean isIntegerParsable(String toCheck){
850: if (toCheck == null) return false;
851: try {
852: Integer.parseInt(toCheck);
853: return true;
854: } catch (NumberFormatException e){
855: return false;
856: }
857: }
858:
859: public static boolean isLongParsable(String toCheck){
860: if (toCheck == null) return false;
861: try {
862: Long.parseLong(toCheck);
863: return true;
864: } catch (NumberFormatException e){
865: return false;
866: }
867: }
868:
869: public static String[] convert(Object... parameters){
870: String[] args = new String[parameters.length];
871:
872: int idx = 0;
873: for(Object object : parameters){
874: args[idx] = String.valueOf(object);
875: idx ++;
876: }
877:
878: return args;
879: }
880:
881: public static String join(Object... parts) {
882: StringBuilder sb = new StringBuilder();
883: for(Object part : parts){
884: if (part == null){
885: continue;
886: }
887: sb.append(String.valueOf(part));
888: }
889: return sb.toString();
890: }
891:
892: /**
893: * Check if the last character in the string matches the input character and
894: * removes. If the match fails, we return the string as it is.
895: *
896: * @param inputString
897: * @param c
898: * @return string
899: */
900: public static String removeLast(String inputString, char c) {
901:
902: if (isNullOrEmpty(inputString)) {
903: return inputString;
904: }
905:
906: if (inputString.charAt(inputString.length() - 1) == c) {
907: return inputString.substring(0, inputString.length() - 1);
908: }
909:
910: return inputString;
911: }
912:
913: public static String stripStringNull(String param) {
914: return (StringUtil.isNullOrEmpty(param) || "null".equalsIgnoreCase(param)) ? StringUtil.EMPTY_STRING : param;
915: }
916:
917: /**
918: *
919: * @param contents
920: * @param key
921: * @param value
922: * @return
923: */
924: public static String convertPatterns(String contents, Map<String, String> keyVals) {
925:
926: if (contents == null) {
927: throw new NullPointerException("Cannot convert null pattern");
928: }
929:
930: for(Map.Entry<String, String> entry : keyVals.entrySet()) {
931: contents = contents.replaceAll(entry.getKey(), entry.getValue());
932: }
933:
934: return contents;
935: }
936:
937: public static String convertPatterns(
938: final String contents,
939: final String hostnamePattern,
940: final String string
941: ) {
942:
943: final Map<String, String> keyVals = new HashMap<String, String>();
944:
945: return convertPatterns(contents, keyVals);
946:
947: }
948:
949: }
Your Eclipse window package explorer should have two files in it now: Bubble.java and Main.java.
Now, you can go ahead and run your code but you will get errors because you don’t have the pictures required by the Bubble.java class to render out the nice looking bubble. So, you need to go create a resources folder and place the pictures inside it.
Then paste in the 3 pictures into the folder. These pictures are also in the main zip file for the entire project at the start of this posting. Or you can download them here.
resources.zip 23 KB
Ok, now go to your Main.java file and run the program. The best way to do this is to move your cursor into the static void main() method and right-click. Choose Run As –> Java Application. This will execute the static void main() and actually run your app. You will see a lot of output in the console window. This is the Zipwhip API giving you lots of debugging feedback.
Now, here’s the big moment. Go ahead and send yourself a text message to the phone you are logged in as. You will get a slick popup window fading in on your desktop in the upper right corner. You can double-click the bubble to jump to the configured URL to reply to your message. You can also hit the reply button or close the bubble.
The key extra chunk of code for this example vs. the previous posting is that we are now listening to signals and parsing them so we can perform a switch. I created an ENUM of some of the standard signals from the Zipwhip API so that I could perform a switch since Java still does not let you do a switch on strings.
Here is the signal observer code and the switch statement.
1: signalClient.addSignalObserver(new SignalObserver() {
2: @Override
3: public void notifySignalReceived(Signal signal) {
4: log.debug("Signal received with uri " + signal.uri);
5:
6: switch (SignalUri.toSignalUri(signal.uri)) {
7: case SIGNAL_MESSAGE_RECEIVE:
8: // We got a message. Let's show it.
9: // The Message object is contained in the signal.content object
10: // but you need to cast it.
11: Message msg = (Message)signal.content;
12: showIncomingMessageAlert(msg);
13: break;
14: case SIGNAL_CONVERSATION_CHANGE:
15: // Do nothing for now
16: break;
17: default:
18: // Do nothing if we don't know the signal
19: break;
20: }
21: }
22:
23: });
You will need to also have the class for the Enum. I got lazy and just placed it into the Main.java file. Technically you should likely place this in its own class file.
1: enum SignalUri
2: {
3: /*
4: * /signal/message/progress
5: * /signal/messageProgress/messageProgress
6: * /signal/message/send
7: * /signal/message/receive
8: * /signal/message/read
9: * /signal/message/delete
10: * /signal/conversation/change
11: */
12: SIGNAL_MESSAGE_RECEIVE,
13: SIGNAL_MESSAGE_PROGRESS,
14: SIGNAL_MESSAGE_READ,
15: SIGNAL_MESSAGE_DELETE,
16: SIGNAL_MESSAGEPROGRESS_MESSAGEPROGRESS,
17: SIGNAL_CONVERSATION_CHANGE,
18: SIGNAL_CONTACT_NEW,
19: SIGNAL_CONTACT_SAVE,
20: SIGNAL_CONTACT_DELETE,
21: NOVALUE;
22:
23: public static SignalUri toSignalUri(String str)
24: {
25: // We are going to use Java's valueOf method, so
26: // we need to cleanup the URI string first
27: // Get rid of first slash
28: String str2 = str.substring(1, str.length());
29: // convert slashes to underscores
30: str2 = str2.replaceAll("/", "_");
31: // go all upper case
32: str2 = str2.toUpperCase();
33:
34: try {
35: return valueOf(str2);
36: }
37: catch (Exception ex) {
38: System.out.println("Found no match for SignalURI:" + str);
39: return NOVALUE;
40: }
41: }
42: }
This posting is about the Zipwhip API so I don’t want to get into the details of the cool looking bubble being generated from the Bubble() class, but it’s worth noting that we are using some of the features only available in later versions of Java that let you create transparent windows. We are doing some fade in/out tricks. We are manually adding mouse listeners to let you drag the window around. We are doing some sprite tricks with getSubImage() so we can repurpose some web PNGs as well. It was quite a lot of work to get that Bubble class setup, but it was well worth it because you can run this app non-stop and actually start using it to get real-time popups.
If you want to try some tricks with the Bubble to see if there are things you like better than the settings I picked, one of the things I recommend is to allow the window focus to be set. If you do this, you will be able to select the text from the message to copy it to the clipboard. Go into the code and change the this.setFocusableWindowState() line:
Notice that you can now select text, hit Ctrl-C to copy it to the clipboard, hit tab to choose Reply, hit the spacebar to choose the button, etc. The reason I didn’t pick allowing focus is that if a bubble pops in while you’re doing something like writing an email, I don’t think it’s a good idea to steal the focus. The user could consider that rude. However, it’s up to you how you want to setup your app.
Ok, thanks for reading the tutorial. Enjoy playing with your popup bubbles on your desktop when your friends text you on your phone.
