Abstracted 'dispatch(Runnable)' so that all updates to the SystemTray

occur on the dispatch thread. This will resolve any race condition
 issues when creating, then (before it's actually created) trying to
 modify a menu entry.
This commit is contained in:
nathan 2016-04-05 14:07:41 +02:00
parent 077aca538c
commit 6fdbe8ac83
5 changed files with 301 additions and 86 deletions

View File

@ -41,6 +41,9 @@ import java.net.URL;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/** /**
@ -430,6 +433,11 @@ class SystemTray {
SystemTray() { SystemTray() {
} }
/**
* Necessary to guarantee all updates occur on the dispatch thread
*/
protected abstract
void dispatch(Runnable runnable);
/** /**
* Must be wrapped in a synchronized block for object visibility * Must be wrapped in a synchronized block for object visibility
@ -593,16 +601,37 @@ class SystemTray {
* @param newMenuText the new menu text (this will replace the original menu text) * @param newMenuText the new menu text (this will replace the original menu text)
*/ */
public final public final
void updateMenuEntry_Text(String origMenuText, String newMenuText) { void updateMenuEntry_Text(final String origMenuText, final String newMenuText) {
synchronized (menuEntries) { // have to wait for the value
MenuEntry menuEntry = getMenuEntry(origMenuText); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);
if (menuEntry == null) { dispatch(new Runnable() {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); @Override
} public
else { void run() {
menuEntry.setText(newMenuText); synchronized (menuEntries) {
MenuEntry menuEntry = getMenuEntry(origMenuText);
if (menuEntry == null) {
hasValue.set(false);
}
else {
menuEntry.setText(newMenuText);
}
}
countDownLatch.countDown();
} }
});
try {
final boolean await = countDownLatch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!hasValue.get()) {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'");
} }
} }
@ -613,16 +642,37 @@ class SystemTray {
* @param imagePath the new path for the image to use or null to delete the image * @param imagePath the new path for the image to use or null to delete the image
*/ */
public final public final
void updateMenuEntry_Image(String origMenuText, String imagePath) { void updateMenuEntry_Image(final String origMenuText, final String imagePath) {
synchronized (menuEntries) { // have to wait for the value
MenuEntry menuEntry = getMenuEntry(origMenuText); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);
if (menuEntry == null) { dispatch(new Runnable() {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); @Override
} public
else { void run() {
menuEntry.setImage(imagePath); synchronized (menuEntries) {
MenuEntry menuEntry = getMenuEntry(origMenuText);
if (menuEntry == null) {
hasValue.set(false);
}
else {
menuEntry.setImage(imagePath);
}
}
countDownLatch.countDown();
} }
});
try {
final boolean await = countDownLatch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!hasValue.get()) {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'");
} }
} }
@ -633,16 +683,38 @@ class SystemTray {
* @param imageUrl the new URL for the image to use or null to delete the image * @param imageUrl the new URL for the image to use or null to delete the image
*/ */
public final public final
void updateMenuEntry_Image(String origMenuText, URL imageUrl) { void updateMenuEntry_Image(final String origMenuText, final URL imageUrl) {
synchronized (menuEntries) { // have to wait for the value
MenuEntry menuEntry = getMenuEntry(origMenuText); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);
if (menuEntry == null) { dispatch(new Runnable() {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); @Override
} public
else { void run() {
menuEntry.setImage(imageUrl); synchronized (menuEntries) {
MenuEntry menuEntry = getMenuEntry(origMenuText);
if (menuEntry == null) {
hasValue.set(false);
}
else {
menuEntry.setImage(imageUrl);
}
}
countDownLatch.countDown();
} }
});
try {
final boolean await = countDownLatch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!hasValue.get()) {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'");
} }
} }
@ -653,16 +725,37 @@ class SystemTray {
* @param imageStream the InputStream of the image to use or null to delete the image * @param imageStream the InputStream of the image to use or null to delete the image
*/ */
public final public final
void updateMenuEntry_Image(String origMenuText, String cacheName, InputStream imageStream) { void updateMenuEntry_Image(final String origMenuText, final String cacheName, final InputStream imageStream) {
synchronized (menuEntries) { // have to wait for the value
MenuEntry menuEntry = getMenuEntry(origMenuText); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);
if (menuEntry == null) { dispatch(new Runnable() {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); @Override
} public
else { void run() {
menuEntry.setImage(cacheName, imageStream); synchronized (menuEntries) {
MenuEntry menuEntry = getMenuEntry(origMenuText);
if (menuEntry == null) {
hasValue.set(false);
}
else {
menuEntry.setImage(cacheName, imageStream);
}
}
countDownLatch.countDown();
} }
});
try {
final boolean await = countDownLatch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!hasValue.get()) {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'");
} }
} }
@ -675,18 +768,39 @@ class SystemTray {
* @param origMenuText the original menu text * @param origMenuText the original menu text
* @param imageStream the new path for the image to use or null to delete the image * @param imageStream the new path for the image to use or null to delete the image
*/ */
@Deprecated
public final public final
void updateMenuEntry_Image(String origMenuText, InputStream imageStream) { void updateMenuEntry_Image(final String origMenuText, final InputStream imageStream) {
synchronized (menuEntries) { // have to wait for the value
MenuEntry menuEntry = getMenuEntry(origMenuText); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);
if (menuEntry == null) { dispatch(new Runnable() {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); @SuppressWarnings("deprecation")
} @Override
else { public
menuEntry.setImage(imageStream); void run() {
synchronized (menuEntries) {
MenuEntry menuEntry = getMenuEntry(origMenuText);
if (menuEntry == null) {
hasValue.set(false);
}
else {
menuEntry.setImage(imageStream);
}
}
countDownLatch.countDown();
} }
});
try {
final boolean await = countDownLatch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!hasValue.get()) {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'");
} }
} }
@ -697,16 +811,37 @@ class SystemTray {
* @param newCallback the new callback (this will replace the original callback) * @param newCallback the new callback (this will replace the original callback)
*/ */
public final public final
void updateMenuEntry_Callback(String origMenuText, SystemTrayMenuAction newCallback) { void updateMenuEntry_Callback(final String origMenuText, final SystemTrayMenuAction newCallback) {
synchronized (menuEntries) { // have to wait for the value
MenuEntry menuEntry = getMenuEntry(origMenuText); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);
if (menuEntry != null) { dispatch(new Runnable() {
menuEntry.setCallback(newCallback); @Override
} public
else { void run() {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); synchronized (menuEntries) {
MenuEntry menuEntry = getMenuEntry(origMenuText);
if (menuEntry == null) {
hasValue.set(false);
}
else {
menuEntry.setCallback(newCallback);
}
}
countDownLatch.countDown();
} }
});
try {
final boolean await = countDownLatch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!hasValue.get()) {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'");
} }
} }
@ -719,17 +854,38 @@ class SystemTray {
* @param newCallback the new callback (this will replace the original callback) * @param newCallback the new callback (this will replace the original callback)
*/ */
public final public final
void updateMenuEntry(String origMenuText, String newMenuText, SystemTrayMenuAction newCallback) { void updateMenuEntry(final String origMenuText, final String newMenuText, final SystemTrayMenuAction newCallback) {
synchronized (menuEntries) { // have to wait for the value
MenuEntry menuEntry = getMenuEntry(origMenuText); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);
if (menuEntry == null) { dispatch(new Runnable() {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); @Override
} public
else { void run() {
menuEntry.setText(newMenuText); synchronized (menuEntries) {
menuEntry.setCallback(newCallback); MenuEntry menuEntry = getMenuEntry(origMenuText);
if (menuEntry == null) {
hasValue.set(false);
}
else {
menuEntry.setText(newMenuText);
menuEntry.setCallback(newCallback);
}
}
countDownLatch.countDown();
} }
});
try {
final boolean await = countDownLatch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!hasValue.get()) {
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'");
} }
} }
@ -747,20 +903,42 @@ class SystemTray {
final String label = menuEntry.getText(); final String label = menuEntry.getText();
synchronized (menuEntries) { // have to wait for the value
for (Iterator<MenuEntry> iterator = menuEntries.iterator(); iterator.hasNext(); ) { final CountDownLatch countDownLatch = new CountDownLatch(1);
final MenuEntry entry = iterator.next(); final AtomicBoolean hasValue = new AtomicBoolean(false);
if (entry.getText()
.equals(label)) {
iterator.remove();
// this will also reset the menu dispatch(new Runnable() {
menuEntry.remove(); @Override
return; public
void run() {
synchronized (menuEntries) {
for (Iterator<MenuEntry> iterator = menuEntries.iterator(); iterator.hasNext(); ) {
final MenuEntry entry = iterator.next();
if (entry.getText()
.equals(label)) {
iterator.remove();
// this will also reset the menu
menuEntry.remove();
hasValue.set(true);
countDownLatch.countDown();
return;
}
}
} }
countDownLatch.countDown();
} }
});
try {
final boolean await = countDownLatch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!hasValue.get()) {
throw new NullPointerException("Menu entry '" + label + "'not found in list while trying to remove it.");
} }
throw new NullPointerException("Menu entry '" + label + "'not found in list while trying to remove it.");
} }
@ -771,15 +949,42 @@ class SystemTray {
*/ */
public final public final
void removeMenuEntry(final String menuText) { void removeMenuEntry(final String menuText) {
synchronized (menuEntries) { // have to wait for the value
MenuEntry menuEntry = getMenuEntry(menuText); final CountDownLatch countDownLatch = new CountDownLatch(1);
final AtomicBoolean hasValue = new AtomicBoolean(true);
if (menuEntry == null) { dispatch(new Runnable() {
throw new NullPointerException("No menu entry exists for string '" + menuText + "'"); @Override
} public
else { void run() {
removeMenuEntry(menuEntry); dispatch(new Runnable() {
@Override
public
void run() {
synchronized (menuEntries) {
MenuEntry menuEntry = getMenuEntry(menuText);
if (menuEntry == null) {
hasValue.set(false);
}
else {
removeMenuEntry(menuEntry);
}
}
countDownLatch.countDown();
}
});
} }
});
try {
final boolean await = countDownLatch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!hasValue.get()) {
throw new NullPointerException("No menu entry exists for string '" + menuText + "'");
} }
} }
} }

View File

@ -43,7 +43,7 @@ class AppIndicatorTray extends GtkTypeSystemTray {
AppIndicatorTray() { AppIndicatorTray() {
GtkSupport.startGui(); GtkSupport.startGui();
GtkSupport.dispatch(new Runnable() { dispatch(new Runnable() {
@Override @Override
public public
void run() { void run() {
@ -57,7 +57,7 @@ class AppIndicatorTray extends GtkTypeSystemTray {
public public
void shutdown() { void shutdown() {
if (!shuttingDown.getAndSet(true)) { if (!shuttingDown.getAndSet(true)) {
GtkSupport.dispatch(new Runnable() { dispatch(new Runnable() {
@Override @Override
public public
void run() { void run() {
@ -77,7 +77,7 @@ class AppIndicatorTray extends GtkTypeSystemTray {
@Override @Override
protected protected
void setIcon_(final String iconPath) { void setIcon_(final String iconPath) {
GtkSupport.dispatch(new Runnable() { dispatch(new Runnable() {
@Override @Override
public public
void run() { void run() {

View File

@ -48,7 +48,7 @@ class GtkSystemTray extends GtkTypeSystemTray {
super(); super();
GtkSupport.startGui(); GtkSupport.startGui();
GtkSupport.dispatch(new Runnable() { dispatch(new Runnable() {
@Override @Override
public public
void run() { void run() {
@ -78,7 +78,7 @@ class GtkSystemTray extends GtkTypeSystemTray {
public public
void shutdown() { void shutdown() {
if (!shuttingDown.getAndSet(true)) { if (!shuttingDown.getAndSet(true)) {
GtkSupport.dispatch(new Runnable() { dispatch(new Runnable() {
@Override @Override
public public
void run() { void run() {
@ -99,7 +99,7 @@ class GtkSystemTray extends GtkTypeSystemTray {
@Override @Override
protected protected
void setIcon_(final String iconPath) { void setIcon_(final String iconPath) {
GtkSupport.dispatch(new Runnable() { dispatch(new Runnable() {
@Override @Override
public public
void run() { void run() {

View File

@ -47,6 +47,11 @@ class GtkTypeSystemTray extends SystemTray {
private volatile Pointer connectionStatusItem; private volatile Pointer connectionStatusItem;
private volatile String statusText = null; private volatile String statusText = null;
@Override
protected
void dispatch(final Runnable runnable) {
GtkSupport.dispatch(runnable);
}
@Override @Override
public public
@ -221,7 +226,7 @@ class GtkTypeSystemTray extends SystemTray {
* Called inside the gdk_threads block * Called inside the gdk_threads block
*/ */
protected protected
void onMenuAdded(final Pointer menu) {}; void onMenuAdded(final Pointer menu) {}
protected protected
Pointer getMenu() { Pointer getMenu() {

View File

@ -98,12 +98,17 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
return this.statusText; return this.statusText;
} }
protected
void dispatch(Runnable runnable) {
SwingUtil.invokeLater(runnable);
}
@Override @Override
public public
void setStatus(final String statusText) { void setStatus(final String statusText) {
this.statusText = statusText; this.statusText = statusText;
SwingUtil.invokeLater(new Runnable() { dispatch(new Runnable() {
@Override @Override
public public
void run() { void run() {
@ -131,7 +136,7 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
@Override @Override
protected protected
void setIcon_(final String iconPath) { void setIcon_(final String iconPath) {
SwingUtil.invokeLater(new Runnable() { dispatch(new Runnable() {
@Override @Override
public public
void run() { void run() {
@ -145,7 +150,7 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
Image trayImage = new ImageIcon(iconPath).getImage() Image trayImage = new ImageIcon(iconPath).getImage()
.getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH); .getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH);
trayImage.flush(); trayImage.flush();
trayIcon = new TrayIcon(trayImage);; trayIcon = new TrayIcon(trayImage);
// appindicators don't support this, so we cater to the lowest common denominator // appindicators don't support this, so we cater to the lowest common denominator
// trayIcon.setToolTip(SwingSystemTray.this.appName); // trayIcon.setToolTip(SwingSystemTray.this.appName);
@ -215,7 +220,7 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
throw new NullPointerException("Menu text cannot be null"); throw new NullPointerException("Menu text cannot be null");
} }
SwingUtil.invokeLater(new Runnable() { dispatch(new Runnable() {
@Override @Override
public public
void run() { void run() {