From a717dfb923010620c9dbe4c16bf5891c853bdb2f Mon Sep 17 00:00:00 2001
From: zzz <zzz@i2pmail.org>
Date: Wed, 9 Mar 2022 05:21:58 -0500
Subject: [PATCH] DTG: Add new ExternalMain class for app context use

that does not require router.jar
and rework other classes as necessary
---
 .../src/net/i2p/desktopgui/ExternalMain.java  | 227 ++++++++++++++++++
 .../i2p/desktopgui/ExternalTrayManager.java   |   4 +-
 .../i2p/desktopgui/InternalTrayManager.java   |   4 +-
 .../src/net/i2p/desktopgui/Main.java          |   4 +-
 .../src/net/i2p/desktopgui/TrayManager.java   |   4 +-
 5 files changed, 235 insertions(+), 8 deletions(-)
 create mode 100644 apps/desktopgui/src/net/i2p/desktopgui/ExternalMain.java

diff --git a/apps/desktopgui/src/net/i2p/desktopgui/ExternalMain.java b/apps/desktopgui/src/net/i2p/desktopgui/ExternalMain.java
new file mode 100644
index 0000000000..7d44c50103
--- /dev/null
+++ b/apps/desktopgui/src/net/i2p/desktopgui/ExternalMain.java
@@ -0,0 +1,227 @@
+package net.i2p.desktopgui;
+
+import java.awt.Image;
+import java.awt.SystemTray;
+import java.awt.Toolkit;
+import java.io.File;
+import java.lang.reflect.Method;
+import java.net.URL;
+
+import javax.swing.SwingUtilities;
+
+import net.i2p.I2PAppContext;
+import net.i2p.app.ClientAppManager;
+import net.i2p.app.ClientApp;
+import net.i2p.app.ClientAppState;
+import net.i2p.app.NotificationService;
+import net.i2p.util.Log;
+import net.i2p.util.SystemVersion;
+
+/**
+ * A simplified Main that does not require router.jar, for App Context only.
+ * Invokes ExternalTrayManager only.
+ * No state tracking, ClientAppManager doesn't care.
+ *
+ * @since 0.9.54
+ */
+public class ExternalMain implements ClientApp, NotificationService {
+
+    private final I2PAppContext _appContext;
+    private final ClientAppManager _mgr;
+    private final Log log;
+    private TrayManager _trayManager;
+
+    private static final String PROP_SWING = "desktopgui.swing";
+
+    public ExternalMain(I2PAppContext ctx, ClientAppManager mgr, String args[]) {
+        _appContext = ctx;
+        _mgr = mgr;
+        log = _appContext.logManager().getLog(ExternalMain.class);
+    }
+
+    public ExternalMain() {
+        _appContext = I2PAppContext.getGlobalContext();
+        _mgr = _appContext.clientAppManager();
+        log = _appContext.logManager().getLog(ExternalMain.class);
+    }
+    
+    public static void main(String[] args) {
+        // early check so we can bail out when started via CLI
+        if (!SystemTray.isSupported()) {
+            System.err.println("SystemTray not supported");
+            return;
+        }
+        ExternalMain main = new ExternalMain();
+        main.beginStartup(args);
+    }
+    
+    /**
+     * Start the tray icon code (loads tray icon in the tray area).
+     * @throws AWTException on startup error, including systray not supported 
+     */
+    private synchronized void startUp() throws Exception {
+        final TrayManager trayManager;
+        boolean useSwingDefault = !(SystemVersion.isWindows() || SystemVersion.isMac());
+        boolean useSwing = _appContext.getProperty(PROP_SWING, useSwingDefault);
+        _trayManager = new ExternalTrayManager(_appContext, useSwing);
+        _trayManager.startManager();
+        if (_mgr != null)
+            _mgr.register(this);
+    }
+
+    /**
+     * Main method launching the application.
+     *
+     * @param args unused
+     */
+    private void beginStartup(String[] args) {
+        String headless = System.getProperty("java.awt.headless");
+        boolean isHeadless = Boolean.parseBoolean(headless);
+        if (isHeadless) {
+        	log.warn("Headless environment: not starting desktopgui!");
+            return;
+        }
+        if (SystemVersion.isMac())
+            setMacTrayIcon();
+        launchForeverLoop();
+
+        // We'll be doing GUI work, so let's stay in the event dispatcher thread.
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    startUp();
+                } catch(Exception e) {
+                    log.error("Failed while running desktopgui!", e);
+                }
+                
+            }
+            
+        });
+    }
+    
+    /**
+     *  Unless we do this, when we start DesktopGUI we get a Java coffee cup
+     *  in the tray.
+     *
+     *  Based on code from https://gist.github.com/bchapuis/1562406 , no apparent license.
+     *  See also https://stackoverflow.com/questions/6006173/how-do-you-change-the-dock-icon-of-a-java-program
+     *
+     *  TODO, if we wanted to add our own menu, see
+     *  https://stackoverflow.com/questions/1319805/java-os-x-dock-menu
+     *
+     *  TODO, if we want to make it bounce, see
+     *  https://stackoverflow.com/questions/15079783/how-to-make-my-app-icon-bounce-in-the-mac-dock
+     *
+     *  TODO, if we want to handle Quit, see
+     *  https://nakkaya.com/2009/04/19/java-osx-integration/
+     *
+     *  @since 0.9.33
+     */
+    @SuppressWarnings("unchecked")
+    private void setMacTrayIcon() {
+        File f = new File(_appContext.getBaseDir(), "docs/themes/console/images/itoopie_sm.png");
+        if (!f.exists())
+            return;
+        try {
+            Class util = Class.forName("com.apple.eawt.Application");
+            Method getApplication = util.getMethod("getApplication", new Class[0]);
+            Object application = getApplication.invoke(util);
+            Class params[] = new Class[1];
+            params[0] = Image.class;
+            Method setDockIconImage = util.getMethod("setDockIconImage", params);
+            URL url = f.toURI().toURL();
+            Image image = Toolkit.getDefaultToolkit().getImage(url);
+            setDockIconImage.invoke(application, image);
+        } catch (Exception e) {
+            if (log.shouldWarn())
+                log.warn("Can't set OSX Dock icon", e);
+        }
+    }
+    
+    /**
+     * Avoids the app terminating because no Window is opened anymore.
+     * More info: http://java.sun.com/javase/6/docs/api/java/awt/doc-files/AWTThreadIssues.html#Autoshutdown
+     */
+    private static void launchForeverLoop() {
+       Runnable r = new Runnable() {
+            public void run() {
+                try {
+                    Object o = new Object();
+                    synchronized (o) {
+                        o.wait();
+                    }
+                } catch (InterruptedException ie) {
+                }
+            }
+        };
+        Thread t = new Thread(r, "DesktopGUI spinner");
+        t.setDaemon(false);
+        t.start();
+    }
+
+    /////// NotificationService methods
+
+    /**
+     *  Send a notification to the user.
+     *
+     *  @param source unsupported
+     *  @param category unsupported
+     *  @param priority unsupported
+     *  @param title for the popup, translated
+     *  @param message translated
+     *  @param path unsupported
+     *  @return 0, or -1 on failure
+     */
+    public int notify(String source, String category, int priority, String title, String message, String path) {
+        TrayManager tm = _trayManager;
+        if (tm == null)
+            return -1;
+        return tm.displayMessage(priority, title, message, path);
+    }
+
+    /**
+     *  Cancel a notification if possible.
+     *  Unsupported.
+     *
+     *  @return false always
+     */
+    public boolean cancel(int id) {
+        return false;
+    }
+
+    /**
+     *  Update the text of a notification if possible.
+     *  Unsupported.
+     *
+     *  @return false always
+     */
+    public boolean update(int id, String title, String message, String path) {
+        return false;
+    }
+
+    /////// ClientApp methods
+
+    public synchronized void startup() {
+        beginStartup(null);
+    }
+
+    public synchronized void shutdown(String[] args) {
+        if (_trayManager != null)
+            _trayManager.stopManager();
+    }
+
+    public ClientAppState getState() {
+        return ClientAppState.INITIALIZED;
+    }
+
+    public String getName() {
+        return "desktopgui";
+    }
+
+    public String getDisplayName() {
+        return "Desktop GUI";
+    }
+
+    /////// end ClientApp methods
+}
diff --git a/apps/desktopgui/src/net/i2p/desktopgui/ExternalTrayManager.java b/apps/desktopgui/src/net/i2p/desktopgui/ExternalTrayManager.java
index 5c0c0a1cc7..c9317b0678 100644
--- a/apps/desktopgui/src/net/i2p/desktopgui/ExternalTrayManager.java
+++ b/apps/desktopgui/src/net/i2p/desktopgui/ExternalTrayManager.java
@@ -22,8 +22,8 @@ import net.i2p.desktopgui.router.RouterManager;
  */
 class ExternalTrayManager extends TrayManager {
 	
-    public ExternalTrayManager(I2PAppContext ctx, Main main, boolean useSwing) {
-        super(ctx, main, useSwing);
+    public ExternalTrayManager(I2PAppContext ctx, boolean useSwing) {
+        super(ctx, useSwing);
     }
 
     public PopupMenu getMainMenu() {
diff --git a/apps/desktopgui/src/net/i2p/desktopgui/InternalTrayManager.java b/apps/desktopgui/src/net/i2p/desktopgui/InternalTrayManager.java
index 1a5d6c42ea..4a5afb5608 100644
--- a/apps/desktopgui/src/net/i2p/desktopgui/InternalTrayManager.java
+++ b/apps/desktopgui/src/net/i2p/desktopgui/InternalTrayManager.java
@@ -30,6 +30,7 @@ class InternalTrayManager extends TrayManager {
 	
     private final RouterContext _context;
     private final Log log;
+    private final Main _main;
     private MenuItem _statusItem, _browserItem, _configItem, _restartItem, _stopItem,
                      _restartHardItem, _stopHardItem, _cancelItem,
                      _notificationItem1, _notificationItem2;
@@ -42,8 +43,9 @@ class InternalTrayManager extends TrayManager {
     private static final String CONSOLE_BUNDLE_NAME = "net.i2p.router.web.messages";
 
     public InternalTrayManager(RouterContext ctx, Main main, boolean useSwing) {
-        super(ctx, main, useSwing);
+        super(ctx, useSwing);
         _context = ctx;
+        _main = main;
         log = ctx.logManager().getLog(InternalTrayManager.class);
     }
 
diff --git a/apps/desktopgui/src/net/i2p/desktopgui/Main.java b/apps/desktopgui/src/net/i2p/desktopgui/Main.java
index aad835cf5e..0a7e3f9bc2 100644
--- a/apps/desktopgui/src/net/i2p/desktopgui/Main.java
+++ b/apps/desktopgui/src/net/i2p/desktopgui/Main.java
@@ -57,7 +57,7 @@ public class Main implements RouterApp, NotificationService {
      */
     public Main() {
         _appContext = I2PAppContext.getGlobalContext();
-        if (_appContext instanceof RouterContext)
+        if (_appContext.isRouterContext())
             _context = (RouterContext) _appContext;
         else
             _context = null;
@@ -77,7 +77,7 @@ public class Main implements RouterApp, NotificationService {
         if (_context != null)
             trayManager = new InternalTrayManager(_context, this, useSwing);
         else
-            trayManager = new ExternalTrayManager(_appContext, this, useSwing);
+            trayManager = new ExternalTrayManager(_appContext, useSwing);
         trayManager.startManager();
         _trayManager = trayManager;
         changeState(RUNNING);
diff --git a/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java b/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java
index 4c16c45d87..f17f8b2d04 100644
--- a/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java
+++ b/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java
@@ -37,7 +37,6 @@ import net.i2p.util.SystemVersion;
 abstract class TrayManager {
 
     protected final I2PAppContext _appContext;
-    protected final Main _main;
     protected final boolean _useSwing;
     ///The tray area, or null if unsupported
     protected SystemTray tray;
@@ -55,9 +54,8 @@ abstract class TrayManager {
     /**
      * Instantiate tray manager.
      */
-    protected TrayManager(I2PAppContext ctx, Main main, boolean useSwing) {
+    protected TrayManager(I2PAppContext ctx, boolean useSwing) {
         _appContext = ctx;
-        _main = main;
         _useSwing = useSwing;
     }
     
-- 
GitLab