From e62b76d2cc7ed993eec015c937b035f0d30ef9f7 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Mon, 18 Jun 2012 22:09:45 +0000
Subject: [PATCH] Big refactor of the router console update subsystem, in
 preparation for implementing out-of-console updaters like i2psnark.

- Add new update interfaces in net.i2p.update
- All update implementations moved to routerconsole update/
- Implement an UpdateManager that registers with the RouterContext
- UpdateManager handles multiple types of things to update
  (router, plugins, news, ...) and methods of updating (HTTP, ...)
- UpdateManager maintains list of installed, downloaded, and available versions of everything
- Define Updaters that can check for a new version and/or download an item
- Individual Updaters register with the UpdateManager obtained from
  I2PAppContext, identifying the type of update item and
  update method they can handle.
- Updaters need only core libs, no router.jar or routerconsole access required.
- All checks and updates are initiated via the UpdateManager.
- All status on checks and updates in-progress or completed are
  obtained from the UpdateManager. No more use of System properties
  to broadcast update state.
- All update and checker tasks are intantiated on demand and threaded;
  no more static references left over.
- Split out the Runners and Checkers from the Handlers and make the inheritance more sane.
- No more permanent NewsFetcher thread; run on the SimpleScheduler queue
  and thread a checker task only to fetch the news.
- No more static NewsFetcher instance in routerconsole.
  All helper methods that are still required are moved to NewsHelper.

The UpdateManager implements the policy for when to check and download.
All requests go through the UpdateManager.

For each update type, there's several parts:
    - The xxxUpdateHandler implements the Updater
    - The xxxUpdateChecker implements the UpdateTask for checking
    - The xxxUpdateRunner implements the UpdateTask for downloading

New and moved classes:

web/				update/
----				-------
new				ConsoleUpdateManager.java

new				PluginUpdateChecker.java from PluginUpdateChecker
PluginUpdateChecker 		-> PluginUpdateHandler.java
PluginUpdateHandler.java	-> PluginUpdateRunner

new				UnsignedUpdateHandler.java
UnsignedUpdateHandler		->  UnsignedUpdateRunner.java
new				UnsignedUpdateChecker from NewsFetcher

UpdateHandler.java remains
new				UpdateHandler.java
new				UpdateRunner.java from UpdateHandler

move				NewsHandler from NewsFetcher
new				NewsFetcher
new				NewsTimerTask

new				DummyHandler


Initial checkin. Unfinished, untested, unpolished.
---
 .../router/update/ConsoleUpdateManager.java   | 1048 +++++++++++++++++
 .../net/i2p/router/update/DummyHandler.java   |   68 ++
 .../net/i2p/router/update/NewsFetcher.java    |  198 ++++
 .../net/i2p/router/update/NewsHandler.java    |   56 +
 .../net/i2p/router/update/NewsTimerTask.java  |  109 ++
 .../router/update/PluginUpdateChecker.java    |   92 ++
 .../router/update/PluginUpdateHandler.java    |   92 ++
 .../PluginUpdateRunner.java}                  |  147 +--
 .../router/update/UnsignedUpdateChecker.java  |   94 ++
 .../router/update/UnsignedUpdateHandler.java  |   99 ++
 .../router/update/UnsignedUpdateRunner.java   |   69 ++
 .../net/i2p/router/update/UpdateHandler.java  |   69 ++
 .../net/i2p/router/update/UpdateRunner.java   |  231 ++++
 .../src/net/i2p/router/update/package.html    |    7 +
 .../src/net/i2p/router/web/CSSHelper.java     |    2 +-
 .../i2p/router/web/ConfigClientsHandler.java  |   39 +-
 .../i2p/router/web/ConfigClientsHelper.java   |    2 +-
 .../i2p/router/web/ConfigUpdateHandler.java   |   22 +-
 .../i2p/router/web/ConfigUpdateHelper.java    |    4 +-
 .../net/i2p/router/web/FileDumpHelper.java    |    2 +-
 .../src/net/i2p/router/web/LogsHelper.java    |    2 +-
 .../src/net/i2p/router/web/NewsFetcher.java   |  416 -------
 .../src/net/i2p/router/web/NewsHelper.java    |  212 +++-
 .../src/net/i2p/router/web/PluginStarter.java |   73 +-
 .../i2p/router/web/PluginUpdateChecker.java   |  200 ----
 .../i2p/router/web/RouterConsoleRunner.java   |   24 +-
 .../src/net/i2p/router/web/SummaryHelper.java |   12 +-
 .../i2p/router/web/UnsignedUpdateHandler.java |  130 --
 .../src/net/i2p/router/web/UpdateHandler.java |  299 +----
 .../i2p/router/web/WebAppConfiguration.java   |    2 +-
 core/java/src/net/i2p/I2PAppContext.java      |   10 +
 .../src/net/i2p/update/UpdateManager.java     |   79 ++
 .../java/src/net/i2p/update/UpdateMethod.java |   15 +
 core/java/src/net/i2p/update/UpdateTask.java  |   30 +
 core/java/src/net/i2p/update/UpdateType.java  |   16 +
 core/java/src/net/i2p/update/Updater.java     |   36 +
 core/java/src/net/i2p/update/package.html     |    8 +
 .../src/net/i2p/util/VersionComparator.java   |   17 +-
 .../src/net/i2p/router/RouterContext.java     |   42 +-
 39 files changed, 2837 insertions(+), 1236 deletions(-)
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/update/DummyHandler.java
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/update/NewsFetcher.java
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/update/NewsHandler.java
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/update/NewsTimerTask.java
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateChecker.java
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java
 rename apps/routerconsole/java/src/net/i2p/router/{web/PluginUpdateHandler.java => update/PluginUpdateRunner.java} (82%)
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateChecker.java
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateHandler.java
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateRunner.java
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/update/UpdateHandler.java
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/update/package.html
 delete mode 100644 apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java
 delete mode 100644 apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java
 delete mode 100644 apps/routerconsole/java/src/net/i2p/router/web/UnsignedUpdateHandler.java
 create mode 100644 core/java/src/net/i2p/update/UpdateManager.java
 create mode 100644 core/java/src/net/i2p/update/UpdateMethod.java
 create mode 100644 core/java/src/net/i2p/update/UpdateTask.java
 create mode 100644 core/java/src/net/i2p/update/UpdateType.java
 create mode 100644 core/java/src/net/i2p/update/Updater.java
 create mode 100644 core/java/src/net/i2p/update/package.html

diff --git a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
new file mode 100644
index 0000000000..5b4ddb1e43
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
@@ -0,0 +1,1048 @@
+package net.i2p.router.update;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.concurrent.ConcurrentHashMap;
+
+import net.i2p.I2PAppContext;
+import net.i2p.crypto.TrustedUpdate;
+import net.i2p.data.DataHelper;
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.router.RouterVersion;
+import net.i2p.router.util.RFC822Date;
+import net.i2p.router.web.ConfigServiceHandler;
+import net.i2p.router.web.ConfigUpdateHandler;
+import net.i2p.router.web.Messages;
+import net.i2p.router.web.NewsHelper;
+import net.i2p.router.web.PluginStarter;
+import net.i2p.update.*;
+import static net.i2p.update.UpdateType.*;
+import static net.i2p.update.UpdateMethod.*;
+import net.i2p.util.ConcurrentHashSet;
+import net.i2p.util.FileUtil;
+import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
+import net.i2p.util.SimpleTimer;
+import net.i2p.util.VersionComparator;
+
+/**
+ *  The central resource coordinating updates.
+ *  This must be registered with the context.
+ *
+ *  The UpdateManager starts and stops all updates,
+ *  prevents multiple updates as appropriate,
+ *  and controls notification to the user.
+ *
+ *  @since 0.9.2
+ */
+public class ConsoleUpdateManager implements UpdateManager {
+    
+    private final RouterContext _context;
+    private final Log _log;
+    /** registered checkers / updaters */
+    private final Collection<RegisteredUpdater> _registered;
+    /** active checking tasks */
+    private final Collection<UpdateTask> _checkers;
+    /** active updating tasks, pointing to the next ones to try */
+    private final Map<UpdateTask, List<RegisteredUpdater>> _downloaders;
+    /** as reported by checkers */
+    private final Map<UpdateItem, VersionAvailable> _available;
+    /** downloaded but NOT installed */
+    private final Map<UpdateItem, Version> _downloaded;
+    /** downloaded but NOT installed */
+    private final Map<UpdateItem, Version> _installed;
+    private final DecimalFormat _pct = new DecimalFormat("0.0%");
+    private static final VersionComparator _versionComparator = new VersionComparator();
+
+    private volatile String _status;
+
+    public ConsoleUpdateManager(RouterContext ctx) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(ConsoleUpdateManager.class);
+        _registered = new ConcurrentHashSet();
+        _checkers = new ConcurrentHashSet();
+        _downloaders = new ConcurrentHashMap();
+        _available = new ConcurrentHashMap();
+        _downloaded = new ConcurrentHashMap();
+        _installed = new ConcurrentHashMap();
+        _status = "";
+    }
+
+    public static ConsoleUpdateManager getInstance() {
+        return (ConsoleUpdateManager) I2PAppContext.getGlobalContext().updateManager();
+    }
+
+    public void start() {
+        notifyInstalled(NEWS, "", Long.toString(NewsHelper.lastUpdated(_context)));
+        notifyInstalled(ROUTER_SIGNED, "", RouterVersion.VERSION);
+        // hack to init from the current news file... do this before we register Updaters
+        (new NewsFetcher(_context, Collections.EMPTY_LIST)).checkForUpdates();
+        for (String plugin : PluginStarter.getPlugins()) {
+            Properties props = PluginStarter.pluginProperties(_context, plugin);
+            String ver = props.getProperty("version");
+            if (ver != null)
+                notifyInstalled(PLUGIN, plugin, ver);
+        }
+
+        _context.registerUpdateManager(this);
+        Updater u = new DummyHandler(_context);
+        register(u, TYPE_DUMMY, HTTP, 0);
+        // register news before router, so we don't fire off an update
+        // right at instantiation if the news is already indicating a new version
+        u = new NewsHandler(_context);
+        register(u, NEWS, HTTP, 0);
+        register(u, ROUTER_SIGNED, HTTP, 0);  // news is an update checker for the router
+        u = new UpdateHandler(_context);
+        register(u, ROUTER_SIGNED, HTTP, 0);
+        u = new UnsignedUpdateHandler(_context);
+        register(u, ROUTER_UNSIGNED, HTTP, 0);
+        u = new PluginUpdateHandler(_context);
+        register(u, PLUGIN, HTTP, 0);
+        register(u, PLUGIN_INSTALL, HTTP, 0);
+        new NewsTimerTask(_context);
+    }
+
+    public void shutdown() {
+        _context.unregisterUpdateManager(this);
+        stopChecks();
+        stopUpdates();
+        _registered.clear();
+        _available.clear();
+        _downloaded.clear();
+        _installed.clear();
+    }
+
+    /**
+     *  The status on any update current or last finished.
+     *  @return status or ""
+     */
+    public String getStatus() {
+        return _status;
+    }
+    
+    public String checkAvailable(UpdateType type, long maxWait) {
+        return checkAvailable(type, "", maxWait);
+    }
+
+    /**
+     *  Is an update available?
+     *  Blocking.
+     *  @param maxWait max time to block
+     *  @return new version or null if nothing newer is available
+     */
+    public String checkAvailable(UpdateType type, String id, long maxWait) {
+//// update too?
+        if (isCheckInProgress(type, id) || isUpdateInProgress(type, id)) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Check or update already in progress for: " + type + ' ' + id);
+            return null;
+        }
+        for (RegisteredUpdater r : _registered) {
+            if (r.type == type) {
+                UpdateTask t = r.updater.check(type, r.method, id, "FIXME", maxWait);
+                if (t != null) {
+                    synchronized(t) {
+                        try {
+                            t.wait(maxWait);
+                        } catch (InterruptedException ie) {}
+                    }
+                    return getUpdateAvailable(type, id);
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     *  Fire off a checker task
+     *  Non-blocking.
+     */
+    public void check(UpdateType type, String id) {
+        if (isCheckInProgress(type, id)) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Check or update already in progress for: " + type + ' ' + id);
+            return;
+        }
+        for (RegisteredUpdater r : _registered) {
+            if (r.type == type) {
+/// fixme "" will put an entry in _available for everything grrrrr????
+                UpdateTask t = r.updater.check(type, r.method, id, "", 5*60*1000);
+                if (t != null)
+                    break;
+            }
+        }
+    }
+
+    /**
+     *  Is an update available?
+     *  Non-blocking, returns result of last check or notification from an Updater
+     *  @return new version or null if nothing newer is available
+     */
+    public String getUpdateAvailable(UpdateType type) {
+        return getUpdateAvailable(type, "");
+    }
+
+    /**
+     *  Is an update available?
+     *  Non-blocking, returns result of last check or notification from an Updater
+     *  @return new version or null if nothing newer is available
+     */
+    public String getUpdateAvailable(UpdateType type, String id) {
+        Version v = _available.get(new UpdateItem(type, id));
+        if (v == null)
+            return null;
+        return v.version;
+    }
+
+    /**
+     *  Is an update downloaded?
+     *  Non-blocking, returns result of last download
+     *  @return new version or null if nothing was downloaded
+     */
+    public String getUpdateDownloaded(UpdateType type) {
+        return getUpdateDownloaded(type, "");
+    }
+
+    /**
+     *  Is an update downloaded?
+     *  Non-blocking, returns result of last download
+     *  @return new version or null if nothing was downloaded
+     */
+    public String getUpdateDownloaded(UpdateType type, String id) {
+        Version v = _downloaded.get(new UpdateItem(type, id));
+        if (v == null)
+            return null;
+        return v.version;
+    }
+
+    /**
+     *  Is any download in progress?
+     *  Does not include checks.
+     */
+    public boolean isUpdateInProgress() {
+        return !_downloaders.isEmpty();
+    }
+
+    /**
+     *  Is a download in progress?
+     */
+    public boolean isUpdateInProgress(UpdateType type) {
+        return isUpdateInProgress(type, "");
+    }
+
+    /**
+     *  Is a download in progress?
+     */
+    public boolean isUpdateInProgress(UpdateType type, String id) {
+        for (UpdateTask t : _downloaders.keySet()) {
+            if (t.getType() == type && id.equals(t.getID()))
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     *  Stop all downloads in progress
+     */
+    public void stopUpdates() {
+        for (UpdateTask t : _downloaders.keySet()) {
+            t.shutdown();
+        }
+        _downloaders.clear();
+    }
+
+    /**
+     *  Stop this download
+     */
+    public void stopUpdate(UpdateType type) {
+        stopUpdate(type, "");
+    }
+
+    /**
+     *  Stop this download
+     */
+    public void stopUpdate(UpdateType type, String id) {
+        for (Iterator<UpdateTask> iter = _downloaders.keySet().iterator(); iter.hasNext(); ) {
+            UpdateTask t = iter.next();
+            if (t.getType() == type && id.equals(t.getID())) {
+                iter.remove();
+                t.shutdown();
+            }
+        }
+    }
+
+    /**
+     *  Is any check in progress?
+     *  Does not include updates.
+     */
+    public boolean isCheckInProgress() {
+        return !_checkers.isEmpty();
+    }
+
+    /**
+     *  Is a check in progress?
+     */
+    public boolean isCheckInProgress(UpdateType type) {
+        return isCheckInProgress(type, "");
+    }
+
+    /**
+     *  Is a check in progress?
+     */
+    public boolean isCheckInProgress(UpdateType type, String id) {
+        for (UpdateTask t : _checkers) {
+            if (t.getType() == type && id.equals(t.getID()))
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     *  Stop all checks in progress
+     */
+    public void stopChecks() {
+        for (UpdateTask t : _checkers) {
+            t.shutdown();
+        }
+        _checkers.clear();
+    }
+
+    /**
+     *  Stop this check
+     */
+    public void stopCheck(UpdateType type) {
+        stopCheck(type, "");
+    }
+
+    /**
+     *  Stop this check
+     */
+    public void stopCheck(UpdateType type, String id) {
+        for (Iterator<UpdateTask> iter = _checkers.iterator(); iter.hasNext(); ) {
+            UpdateTask t = iter.next();
+            if (t.getType() == type && id.equals(t.getID())) {
+                iter.remove();
+                t.shutdown();
+            }
+        }
+    }
+
+    /**
+     *  Install a plugin. Non-blocking.
+     *  If returns true, then call isUpdateInProgress() in a loop
+     *  @return true if task started
+     */
+    public boolean installPlugin(URI uri) {
+        String fakeName = Long.toString(_context.random().nextLong());
+        List<URI> uris = Collections.singletonList(uri);
+        UpdateItem fake = new UpdateItem(PLUGIN_INSTALL, fakeName);
+        VersionAvailable va = new VersionAvailable("", "", HTTP, uris);
+        _available.put(fake, va);
+        return update(PLUGIN_INSTALL, fakeName);
+    }
+
+    /**
+     *  Non-blocking. Does not check.
+     *  If returns true, then call isUpdateInProgress() in a loop
+     *  Max time 3 hours by default but not honored by all Updaters
+     *  @return true if task started
+     */
+    public boolean update(UpdateType type) {
+        return update(type, "", 3*60*1000);
+    }
+
+    /**
+     *  Non-blocking. Does not check.
+     *  Max time 3 hours by default but not honored by all Updaters
+     *  If returns true, then call isUpdateInProgress() in a loop
+     *  @return true if task started
+     */
+    public boolean update(UpdateType type, String id) {
+        return update(type, id, 3*60*60*1000);
+    }
+
+    /**
+     *  Non-blocking. Does not check.
+     *  If returns true, then call isUpdateInProgress() in a loop
+     *  @param maxTime not honored by all Updaters
+     *  @return true if task started
+     */
+    public boolean update(UpdateType type, long maxTime) {
+        return update(type, "", maxTime);
+    }
+
+    /**
+     *  Non-blocking. Does not check.
+     *  If returns true, then call isUpdateInProgress() in a loop
+     *  @param maxTime not honored by all Updaters
+     *  @return true if task started
+     */
+    public boolean update(UpdateType type, String id, long maxTime) {
+        if (isCheckInProgress(type, id) || isUpdateInProgress(type, id)) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Check or update already in progress for: " + type + ' ' + id);
+            return false;
+        }
+        List<URI> updateSources = null;
+        UpdateItem ui = new UpdateItem(type, id);
+        VersionAvailable va = _available.get(ui);
+        if (va == null)
+            return false;
+        List<RegisteredUpdater> sorted = new ArrayList(_registered);
+        Collections.sort(sorted);
+        return retry(ui, va.sourceMap, sorted, maxTime) != null;
+    }
+
+    private UpdateTask retry(UpdateItem ui,
+                             Map<UpdateMethod, List<URI>> sourceMap,
+                             List<RegisteredUpdater> toTry, long maxTime) {
+        for (Iterator<RegisteredUpdater> iter = toTry.iterator(); iter.hasNext(); ) {
+            RegisteredUpdater r = iter.next();
+            iter.remove();
+            // check in case unregistered later
+            if (!_registered.contains(r))
+                continue;
+            for (Map.Entry<UpdateMethod, List<URI>> e : sourceMap.entrySet()) {
+                UpdateMethod meth = e.getKey();
+                if (r.type == ui.type && r.method == meth) {
+                                                                                    // fixme
+                    UpdateTask t = r.updater.update(ui.type, meth, e.getValue(), ui.id, "", maxTime);
+                    if (t != null) {
+                        // race window here
+                        //  store the remaining ones for retrying
+                        _downloaders.put(t, toTry);
+                        return t;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /////////// start UpdateManager interface
+
+    /**
+     *  Call multiple times for each type/method pair.
+     */
+    public void register(Updater updater, UpdateType type, UpdateMethod method, int priority) {
+        _registered.add(new RegisteredUpdater(updater, type, method, priority));
+    }
+
+    public void unregister(Updater updater, UpdateType type, UpdateMethod method) {
+        _registered.remove(new RegisteredUpdater(updater, type, method, 0));
+    }
+    
+    /**
+     *  Called by the Updater, either after check() was called, or it found out on its own.
+     *
+     *  @param newsSource who told us
+     *  @param id plugin name for plugins, ignored otherwise
+     *  @param updateSourcew Where to get the new version
+     *  @param newVersion The new version available
+     *  @param minVersion The minimum installed version to be able to update to newVersion
+     *  @return true if it's newer
+     */
+    public boolean notifyVersionAvailable(UpdateTask task, URI newsSource,
+                                          UpdateType type, String id,
+                                          UpdateMethod method, List<URI> updateSources,
+                                          String newVersion, String minVersion) {
+        if (type == NEWS) {
+            // shortcut
+            notifyInstalled(NEWS, "", newVersion);
+            return true;
+        }
+        UpdateItem ui = new UpdateItem(type, id);
+        VersionAvailable newVA = new VersionAvailable(newVersion, minVersion, method, updateSources);
+        Version old = _installed.get(ui);
+        if (old != null && old.compareTo(newVA) >= 0) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn(ui.toString() + ' ' + old + " already installed");
+            return false;
+        }
+        old = _downloaded.get(ui);
+        if (old != null && old.compareTo(newVA) >= 0) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn(ui.toString() + ' ' + old + " already downloaded");
+            return false;
+        }
+        VersionAvailable oldVA = _available.get(ui);
+        if (oldVA != null)  {
+            int comp = oldVA.compareTo(newVA);
+            if (comp > 0) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn(ui.toString() + ' ' + oldVA + " already available");
+                return false;
+            }
+            if (comp == 0) {
+                if (oldVA.sourceMap.putIfAbsent(method, updateSources) == null) {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn(ui.toString() + ' ' + oldVA + " updated with new source method");
+                } else {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn(ui.toString() + ' ' + oldVA + " already available");
+                }
+                return false;
+            }
+        }
+
+        if (_log.shouldLog(Log.INFO))
+            _log.info(ui.toString() + ' ' + newVA + " now available");
+        _available.put(ui, newVA);
+
+        switch (type) {
+            case NEWS:
+                break;
+
+            case ROUTER_SIGNED:
+            case ROUTER_SIGNED_PACK200:
+                if (shouldInstall()) {
+////////////
+                }
+                break;
+
+            case ROUTER_UNSIGNED:
+                if (shouldInstall()) {
+////////////
+                }
+                break;
+
+            case PLUGIN:
+                String msg = "<b>" + _("New plugin version {0} is available", newVersion) + "</b>";
+                finishStatus(msg);
+                break;
+
+            default:
+                break;
+        }
+        return true;
+// TODO
+    }
+
+    /**
+     *  Called by the Updater after check() was called and all notifyVersionAvailable() callbacks are finished
+     */
+    public void notifyCheckComplete(UpdateTask task, boolean newer, boolean success) {
+        if (_log.shouldLog(Log.INFO))
+            _log.info(task.toString() + " complete");
+        _checkers.remove(task);
+        switch (task.getType()) {
+            case NEWS:
+                // NewsFetcher will notify and spin off update tasks
+                break;
+
+            case ROUTER_SIGNED:
+            case ROUTER_SIGNED_PACK200:
+                break;
+
+            case ROUTER_UNSIGNED:
+                // if  _mgr.getUpdateDownloaded(ROUTER_SIGNED) != null;
+                break;
+
+            case PLUGIN:
+                String msg = null;
+                if (!success)
+                    msg = "<b>" + _("Update check failed for plugin {0}", task.getID()) + "</b>";
+                else if (!newer)
+                    msg = "<b>" + _("No new version is available for plugin {0}", task.getID()) + "</b>";
+                /// else success.... message for that?
+
+                if (msg != null)
+                    finishStatus(msg);
+                break;
+
+            default:
+                break;
+        }
+        synchronized(task) {
+            task.notifyAll();
+        }
+// TODO
+    }
+
+    public void notifyProgress(UpdateTask task, String status, long downloaded, long totalSize) {
+        StringBuilder buf = new StringBuilder(64);
+        buf.append(status);
+        double pct = ((double)downloaded) / ((double)totalSize);
+        synchronized (_pct) {
+            buf.append(_pct.format(pct));
+        }
+        buf.append(":<br>\n");
+        buf.append(_("{0}B transferred", DataHelper.formatSize2(downloaded)));
+        updateStatus(buf.toString());
+    }
+
+    /**
+     *  @param task may be null
+     */
+    public void notifyProgress(UpdateTask task, String status) {
+        updateStatus(status);
+    }
+
+    /**
+     *  An expiring status
+     *  @param task may be null
+     */
+    public void notifyComplete(UpdateTask task, String status) {
+        finishStatus(status);
+    }
+
+    /**
+     *  Not necessarily the end if there are more URIs to try.
+     *  @param t may be null
+     */
+    public void notifyAttemptFailed(UpdateTask task, String reason, Throwable t) {
+        _log.warn("Attempt failed " + task + ": " + reason, t);
+    }
+
+    /**
+     *  The task has finished and failed.
+     *  @param t may be null
+     */
+    public void notifyTaskFailed(UpdateTask task, String reason, Throwable t) {
+        if (_log.shouldLog(Log.WARN))
+            _log.warn("Failed " + task + ": " + reason, t);
+        List<RegisteredUpdater> toTry = _downloaders.get(task);
+        if (toTry != null) {
+            UpdateItem ui = new UpdateItem(task.getType(), task.getID());
+            VersionAvailable va = _available.get(ui);
+            if (va != null) {
+                UpdateTask next = retry(ui, va.sourceMap, toTry, 3*60*1000);  // fixme old maxtime lost
+                if (next != null) {
+                   if (_log.shouldLog(Log.WARN))
+                       _log.warn("Retrying with " + next);
+                }
+            }
+        }
+        _downloaders.remove(task);
+///// for certain types only
+        finishStatus("<b>" + _("Transfer failed from {0}", linkify(task.getURI().toString())) + "</b>");
+    }
+
+    /**
+     *  An update has been downloaded but not verified.
+     *  The manager will verify it.
+     *  Caller should delete the file upon return, unless it will share it with others,
+     *  e.g. on a torrent.
+     *  If the return value is false, caller must call notifyTaskFailed() or notifyComplete()
+     *  again.
+     *
+     *  @param actualVersion may be higher (or lower?) than the version requested
+     *  @param file a valid format for the task's UpdateType
+     *  @return true if valid, false if corrupt
+     */
+    public boolean notifyComplete(UpdateTask task, String actualVersion, File file) {
+        if (_log.shouldLog(Log.INFO))
+            _log.info(task.toString() + " complete");
+        boolean rv = false;
+        switch (task.getType()) {
+            case TYPE_DUMMY:
+            case NEWS:
+                rv = true;
+                break;
+
+            case ROUTER_SIGNED:
+            case ROUTER_SIGNED_PACK200:
+                rv = handleSudFile(task.getURI(), actualVersion, file);
+                if (rv)
+                    notifyDownloaded(task.getType(), task.getID(), actualVersion);
+                break;
+
+            case ROUTER_UNSIGNED:
+                rv = handleUnsignedFile(task.getURI(), actualVersion, file);
+/////// FIXME RFC822 or long?
+                if (rv)
+                    notifyDownloaded(task.getType(), task.getID(), actualVersion);
+                break;
+
+            case PLUGIN:
+/// FIXME probably handled in PluginUpdateRunner??????????
+                rv = handlePluginFile(task.getURI(), actualVersion, file);
+                break;
+
+            default:
+                break;
+        }
+        if (rv)
+            _downloaders.remove(task);
+        return rv;
+    }
+
+    ///////// End UpdateManager interface
+
+    /**
+     *  Adds to installed, removes from downloaded and available
+     *  @param version null to remove from installed
+     */
+    private void notifyInstalled(UpdateType type, String id, String version) {
+        UpdateItem ui = new UpdateItem(type, id);
+        if (version == null) {
+            _installed.remove(ui);
+            if (_log.shouldLog(Log.INFO))
+                _log.info(ui + " removed");
+            return;
+        }
+        Version ver = new Version(version);
+        if (_log.shouldLog(Log.INFO))
+            _log.info(ui + " " + ver + " installed");
+        _installed.put(ui, ver);
+        Version old = _downloaded.get(ui);
+        if (old != null && old.compareTo(ver) <= 0)
+            _downloaded.remove(ui);
+        old = _available.get(ui);
+        if (old != null && old.compareTo(ver) <= 0)
+            _available.remove(ui);
+    }
+
+    /**
+     *  Adds to downloaded, removes from available
+     */
+    private void notifyDownloaded(UpdateType type, String id, String version) {
+        UpdateItem ui = new UpdateItem(type, id);
+        Version ver = new Version(version);
+        if (_log.shouldLog(Log.INFO))
+            _log.info(ui + " " + ver + " downloaded");
+        _downloaded.put(ui, ver);
+        // one trumps the other
+        if (type == ROUTER_SIGNED)
+            _downloaded.remove(new UpdateItem(ROUTER_UNSIGNED, ""));
+        else if (type == ROUTER_UNSIGNED)
+            _downloaded.remove(new UpdateItem(ROUTER_SIGNED, ""));
+        Version old = _available.get(ui);
+        if (old != null && old.compareTo(ver) <= 0)
+            _available.remove(ui);
+    }
+    
+    /** from NewsFetcher */
+    private boolean shouldInstall() {
+        String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY);
+        if ("notify".equals(policy) || NewsHelper.dontInstall(_context))
+            return false;
+//////////////////
+        File zip = new File(_context.getRouterDir(), Router.UPDATE_FILE);
+        return !zip.exists();
+    }
+    
+    /**
+     *  Where to find various resources
+     *  @return non-null may be empty
+     */
+    public List<URI> getUpdateURLs(UpdateType type, String id, UpdateMethod method) {
+        VersionAvailable va = _available.get(new UpdateItem(type, id));
+        if (va != null) {
+            List<URI> rv = va.sourceMap.get(method);
+            if (rv != null)
+                return rv;
+        }
+
+        switch (type) {
+            case NEWS:
+                // handled in NewsHandler
+                break;
+
+            case ROUTER_SIGNED:
+            case ROUTER_SIGNED_PACK200:
+                String URLs = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_URL, ConfigUpdateHandler.DEFAULT_UPDATE_URL);
+                StringTokenizer tok = new StringTokenizer(URLs, " ,\r\n");
+                List<URI> rv = new ArrayList();
+                while (tok.hasMoreTokens()) {
+                    try {
+                        rv.add(new URI(tok.nextToken().trim()));
+                    } catch (URISyntaxException use) {}
+                }
+                Collections.shuffle(rv, _context.random());
+                return rv;
+
+            case ROUTER_UNSIGNED:
+                String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
+                if (url != null) {
+                    try {
+                        return Collections.singletonList(new URI(url));
+                    } catch (URISyntaxException use) {}
+                }
+                break;
+
+            case PLUGIN:
+                Properties props = PluginStarter.pluginProperties(_context, id);
+                String oldVersion = props.getProperty("version");
+                String xpi2pURL = props.getProperty("updateURL");
+                if (xpi2pURL != null) {
+                    try {
+                        return Collections.singletonList(new URI(xpi2pURL));
+                    } catch (URISyntaxException use) {}
+                }
+                break;
+
+             default:
+                break;
+        }
+        return Collections.EMPTY_LIST;
+    }
+
+    /**
+     *  @return success
+     */
+    private boolean handleSudFile(URI uri, String actualVersion, File f) {
+        String url = uri.toString();
+        // Process the .sud/.su2 file
+        updateStatus("<b>" + _("Update downloaded") + "</b>");
+        TrustedUpdate up = new TrustedUpdate(_context);
+        File to = new File(_context.getRouterDir(), Router.UPDATE_FILE);
+        String err = up.migrateVerified(RouterVersion.VERSION, f, to);
+///////////
+        // caller must delete now.. why?
+        //f.delete();
+        if (err == null) {
+            String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY);
+            // So unsigned update handler doesn't overwrite unless newer.
+/// FIXME
+            //String lastmod = _get.getLastModified();
+            String lastmod = null;
+            long modtime = 0;
+            if (lastmod != null)
+                modtime = RFC822Date.parse822Date(lastmod);
+            if (modtime <= 0)
+                modtime = _context.clock().now();
+            _context.router().saveConfig(NewsHelper.PROP_LAST_UPDATE_TIME, "" + modtime);
+
+            if ("install".equals(policy)) {
+                _log.log(Log.CRIT, "Update was VERIFIED, restarting to install it");
+                updateStatus("<b>" + _("Update verified") + "</b><br>" + _("Restarting"));
+                restart();
+            } else {
+                _log.log(Log.CRIT, "Update was VERIFIED, will be installed at next restart");
+                StringBuilder buf = new StringBuilder(64);
+                buf.append("<b>").append(_("Update downloaded")).append("<br>");
+                if (_context.hasWrapper())
+                    buf.append(_("Click Restart to install"));
+                else
+                    buf.append(_("Click Shutdown and restart to install"));
+                if (up.newVersion() != null)
+                    buf.append(' ').append(_("Version {0}", up.newVersion()));
+                buf.append("</b>");
+                updateStatus(buf.toString());
+            }
+        } else {
+            _log.log(Log.CRIT, err + " from " + url);
+            updateStatus("<b>" + err + ' ' + _("from {0}", linkify(url)) + " </b>");
+        }
+        return err == null;
+    }
+
+    /**
+     *  @return success
+     */
+    private boolean handleUnsignedFile(URI uri, String lastmod, File updFile) {
+        if (FileUtil.verifyZip(updFile)) {
+            updateStatus("<b>" + _("Update downloaded") + "</b>");
+        } else {
+            updFile.delete();
+            String url = uri.toString();
+            updateStatus("<b>" + _("Unsigned update file from {0} is corrupt", url) + "</b>");
+            _log.log(Log.CRIT, "Corrupt zip file from " + url);
+            return false;
+        }
+        File to = new File(_context.getRouterDir(), Router.UPDATE_FILE);
+        boolean copied = FileUtil.copy(updFile, to, true, false);
+        if (copied) {
+            updFile.delete();
+            String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY);
+            long modtime = 0;
+            if (lastmod != null)
+                modtime = RFC822Date.parse822Date(lastmod);
+            if (modtime <= 0)
+                modtime = _context.clock().now();
+            _context.router().saveConfig(NewsHelper.PROP_LAST_UPDATE_TIME, "" + modtime);
+            if ("install".equals(policy)) {
+                _log.log(Log.CRIT, "Update was downloaded, restarting to install it");
+                updateStatus("<b>" + _("Update downloaded") + "</b><br>" + _("Restarting"));
+                restart();
+            } else {
+                _log.log(Log.CRIT, "Update was downloaded, will be installed at next restart");
+                StringBuilder buf = new StringBuilder(64);
+                buf.append("<b>").append(_("Update downloaded")).append("</b><br>");
+                if (_context.hasWrapper())
+                    buf.append(_("Click Restart to install"));
+                else
+                    buf.append(_("Click Shutdown and restart to install"));
+/// OK?
+                    buf.append(' ').append(_("Version {0}", lastmod));
+                updateStatus(buf.toString());
+            }
+        } else {
+            _log.log(Log.CRIT, "Failed copy to " + to);
+            updateStatus("<b>" + _("Failed copy to {0}", to.getAbsolutePath()) + "</b>");
+        }
+        return copied;
+    }
+
+    /**
+     *  @return success
+     */
+    private boolean handlePluginFile(URI uri, String actualVersion, File sudFile) {
+       //////////////// handled elsewhere?
+        return false;
+    }
+
+    private void restart() {
+        if (_context.hasWrapper())
+            ConfigServiceHandler.registerWrapperNotifier(_context, Router.EXIT_GRACEFUL_RESTART, false);
+        _context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
+    }
+
+    static String linkify(String url) {
+        return "<a target=\"_blank\" href=\"" + url + "\"/>" + url + "</a>";
+    }
+
+    /** translate a string */
+    private String _(String s) {
+        return Messages.getString(s, _context);
+    }
+
+    /**
+     *  translate a string with a parameter
+     */
+    private String _(String s, Object o) {
+        return Messages.getString(s, o, _context);
+    }
+
+    private void updateStatus(String s) {
+        _status = s;
+    }
+
+    private void finishStatus(String msg) {
+        updateStatus(msg);
+        SimpleScheduler.getInstance().addEvent(new Cleaner(msg), 20*60*1000);
+    }
+
+    private class Cleaner implements SimpleTimer.TimedEvent {
+        private final String _msg;
+        public Cleaner(String msg) {
+            _msg = msg;
+        }
+        public void timeReached() {
+            if (_msg.equals(getStatus()))
+                updateStatus("");
+        }
+    }
+
+    /**
+     *  Equals on updater, type and method only
+     */
+    private static class RegisteredUpdater implements Comparable<RegisteredUpdater> {
+        public final Updater updater;
+        public final UpdateType type;
+        public final UpdateMethod method;
+        public final int priority;
+
+        public RegisteredUpdater(Updater u, UpdateType t, UpdateMethod m, int priority) {
+            updater = u; type = t; method = m; this.priority = priority;
+        }
+
+        /** reverse, highest priority first, ensure different ones are different */
+        public int compareTo(RegisteredUpdater r) {
+            int p = r.priority - priority;
+            if (p != 0)
+                return p;
+            return hashCode() - r.hashCode();
+        }
+
+        @Override
+        public int hashCode() {
+            return updater.hashCode() ^ type.hashCode() ^ method.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof RegisteredUpdater))
+                return false;
+            RegisteredUpdater r = (RegisteredUpdater) o;
+            return type == r.type && method == r.method &&
+                   updater.equals(r.updater);
+        }
+
+        @Override
+        public String toString() {
+            return "RegisteredUpdater " + updater + ' ' + type + ' ' + method + ' ' + priority;
+        }
+    }
+
+    /**
+     *  Equals on type and ID only
+     */
+    private static class UpdateItem {
+        public final UpdateType type;
+        public final String id;
+
+        public UpdateItem(UpdateType t, String id) {
+            type = t;
+            this.id = id;
+        }
+
+        @Override
+        public int hashCode() {
+            return type.hashCode() ^ id.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof UpdateItem))
+                return false;
+            UpdateItem r = (UpdateItem) o;
+            return type == r.type && id.equals(r.id);
+        }
+
+        @Override
+        public String toString() {
+            return "UpdateItem " + type + ' ' + id;
+        }
+    }
+
+    private static class Version implements Comparable<Version> {
+        public final String version;
+
+        public Version(String version) {
+            this.version = version;
+        }
+
+        public int compareTo(Version r) {
+            return _versionComparator.compare(version, r.version);
+        }
+
+        @Override
+        public String toString() {
+            return "Version " + version;
+        }
+    }
+
+    private static class VersionAvailable extends Version {
+        public final String minVersion;
+        public final ConcurrentHashMap<UpdateMethod, List<URI>> sourceMap;
+
+        /**
+         * Puts the method and sources in the map. The map may be added to later.
+         */
+        public VersionAvailable(String version, String min, UpdateMethod method, List<URI> updateSources) {
+            super(version);
+            minVersion = min;
+            sourceMap = new ConcurrentHashMap(4);
+            sourceMap.put(method, updateSources);
+        }
+
+        @Override
+        public String toString() {
+            return "VersionAvailable " + version + ' ' + sourceMap;
+        }
+    }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/DummyHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/DummyHandler.java
new file mode 100644
index 0000000000..04b820ec41
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/update/DummyHandler.java
@@ -0,0 +1,68 @@
+package net.i2p.router.update;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+
+import net.i2p.router.RouterContext;
+import net.i2p.update.*;
+
+/**
+ * Dummy to lock up the updates for a period of time
+ *
+ * @since 0.9.2
+ */
+class DummyHandler implements Updater {
+    private final RouterContext _context;
+    
+    public DummyHandler(RouterContext ctx) {
+        _context = ctx;
+    }
+
+    /**
+     *  Spins off an UpdateTask that sleeps
+     */
+    public UpdateTask check(UpdateType type, UpdateMethod method,
+                            String id, String currentVersion, long maxTime) {
+        if (type != UpdateType.TYPE_DUMMY)
+            return null;
+         return new DummyRunner(_context, maxTime);
+    }
+
+    /**
+     *  Spins off an UpdateTask that sleeps
+     */
+    public UpdateTask update(UpdateType type, UpdateMethod method, List<URI> updateSources,
+                             String id, String newVersion, long maxTime) {
+        if (type != UpdateType.TYPE_DUMMY)
+            return null;
+         return new DummyRunner(_context, maxTime);
+    }
+
+    /**
+     *  Use for both check and update
+     */
+    private static class DummyRunner extends UpdateRunner {
+        private final long _delay;
+
+        public DummyRunner(RouterContext ctx, long maxTime) {
+            super(ctx, Collections.EMPTY_LIST);
+            _delay = maxTime;
+        }
+
+        @Override
+        public UpdateType getType() { return UpdateType.TYPE_DUMMY; }
+
+        @Override
+        protected void update() {
+            try {
+                Thread.sleep(_delay);
+            } catch (InterruptedException ie) {}
+            UpdateManager mgr = _context.updateManager();
+            if (mgr != null) {
+                mgr.notifyCheckComplete(this, false, false);
+                mgr.notifyTaskFailed(this, "dummy", null);
+            }
+        }
+    }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/NewsFetcher.java b/apps/routerconsole/java/src/net/i2p/router/update/NewsFetcher.java
new file mode 100644
index 0000000000..5d74a8a4b8
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/update/NewsFetcher.java
@@ -0,0 +1,198 @@
+package net.i2p.router.update;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import net.i2p.crypto.TrustedUpdate;
+import net.i2p.data.DataHelper;
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.router.RouterVersion;
+import net.i2p.router.util.RFC822Date;
+import net.i2p.router.web.ConfigUpdateHandler;
+import net.i2p.router.web.ConfigUpdateHelper;
+import net.i2p.router.web.NewsHelper;
+import net.i2p.update.*;
+import static net.i2p.update.UpdateType.*;
+import static net.i2p.update.UpdateMethod.*;
+import net.i2p.util.EepGet;
+import net.i2p.util.EepHead;
+import net.i2p.util.FileUtil;
+import net.i2p.util.Log;
+
+/**
+ * Task to fetch updates to the news.xml, and to keep
+ * track of whether that has an announcement for a new version.
+ *
+ * @since 0.9.2 moved from NewsFetcher and make an Updater
+ */
+class NewsFetcher extends UpdateRunner {
+    private String _lastModified;
+    private final File _newsFile;
+    private final File _tempFile;
+
+    private static final String TEMP_NEWS_FILE = "news.xml.temp";
+    
+    public NewsFetcher(RouterContext ctx, List<URI> uris) { 
+        super(ctx, uris);
+        _newsFile = new File(ctx.getRouterDir(), NewsHelper.NEWS_FILE);
+        _tempFile = new File(ctx.getTempDir(), "tmp-" + ctx.random().nextLong() + TEMP_NEWS_FILE);
+        long lastMod = NewsHelper.lastChecked(ctx);
+        if (lastMod > 0)
+            _lastModified = RFC822Date.to822Date(lastMod);
+    }
+
+    @Override
+    public UpdateType getType() { return NEWS; }
+
+    private boolean dontInstall() {
+        return NewsHelper.dontInstall(_context);
+    }
+
+    private boolean shouldInstall() {
+        String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY);
+        if ("notify".equals(policy) || dontInstall())
+            return false;
+        File zip = new File(_context.getRouterDir(), Router.UPDATE_FILE);
+        return !zip.exists();
+    }
+    
+    @Override
+    public void update() {
+        fetchNews();
+    }
+
+    public void fetchNews() {
+        boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
+        String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
+        int proxyPort = ConfigUpdateHandler.proxyPort(_context);
+
+        for (URI uri : _urls) {
+             _currentURI = uri;
+             String newsURL = uri.toString();
+
+            if (_tempFile.exists())
+                _tempFile.delete();
+        
+            try {
+                EepGet get;
+                if (shouldProxy)
+                    get = new EepGet(_context, true, proxyHost, proxyPort, 0, _tempFile.getAbsolutePath(), newsURL, true, null, _lastModified);
+                else
+                    get = new EepGet(_context, false, null, 0, 0, _tempFile.getAbsolutePath(), newsURL, true, null, _lastModified);
+                get.addStatusListener(this);
+                if (get.fetch()) {
+                    String lastMod = get.getLastModified();
+                    if (lastMod != null) {
+                        _lastModified = lastMod;
+                        long lm = RFC822Date.parse822Date(lastMod);
+                        if (lm > 0)
+                            _context.router().saveConfig(NewsHelper.PROP_LAST_CHECKED, Long.toString(lm));
+                    }
+                    return;
+                }
+            } catch (Throwable t) {
+                _log.error("Error fetching the news", t);
+            }
+        }
+    }
+    
+    private static final String VERSION_STRING = "version=\"" + RouterVersion.VERSION + "\"";
+    private static final String VERSION_PREFIX = "version=\"";
+
+///// move to UpdateManager?
+
+    /**
+     *  Parse the installed (not the temp) news file for the latest version.
+     *  TODO: Real XML parsing, different update methods,
+     *  URLs in the file, ...
+     */
+    void checkForUpdates() {
+        FileInputStream in = null;
+        try {
+            in = new FileInputStream(_newsFile);
+            StringBuilder buf = new StringBuilder(128);
+            while (DataHelper.readLine(in, buf)) {
+                int index = buf.indexOf(VERSION_PREFIX);
+                if (index >= 0) {
+                    int end = buf.indexOf("\"", index + VERSION_PREFIX.length());
+                    if (end > index) {
+                        String ver = buf.substring(index+VERSION_PREFIX.length(), end);
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug("Found version: [" + ver + "]");
+                        if (TrustedUpdate.needsUpdate(RouterVersion.VERSION, ver)) {
+                            if (_log.shouldLog(Log.DEBUG))
+                                _log.debug("Our version is out of date, update!");
+                            _mgr.notifyVersionAvailable(this, _currentURI,
+                                                        ROUTER_SIGNED, "", HTTP,
+                                                        _mgr.getUpdateURLs(ROUTER_SIGNED, "", HTTP),
+                                                        ver, "");
+                        } else {
+                            if (_log.shouldLog(Log.DEBUG))
+                                _log.debug("Our version is current");
+                        }
+                        return;
+                    }
+                }
+                if (buf.indexOf(VERSION_STRING) == -1) {
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("No match in " + buf.toString());
+                }
+                buf.setLength(0);
+            }
+        } catch (IOException ioe) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Error checking the news for an update", ioe);
+            return;
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+        }
+        
+        if (_log.shouldLog(Log.WARN))
+            _log.warn("No version found in news.xml file");
+    }
+    
+    /** override to prevent status update */
+    @Override
+    public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {}
+
+    /**
+     *  Copies the file from temp dir to the news location,
+     *  calls checkForUpdates()
+     */
+    @Override
+    public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
+        if (_log.shouldLog(Log.INFO))
+            _log.info("News fetched from " + url + " with " + (alreadyTransferred+bytesTransferred));
+        
+        long now = _context.clock().now();
+        if (_tempFile.exists()) {
+            boolean copied = FileUtil.copy(_tempFile, _newsFile, true, false);
+            _tempFile.delete();
+            if (copied) {
+                String newVer = Long.toString(now);
+                _context.router().saveConfig(NewsHelper.PROP_LAST_UPDATED, newVer);
+                _mgr.notifyVersionAvailable(this, _currentURI, NEWS, "", HTTP,
+                                            null, newVer, "");
+                checkForUpdates();
+            } else {
+                if (_log.shouldLog(Log.ERROR))
+                    _log.error("Failed to copy the news file!");
+            }
+        } else {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Transfer complete, but no file? - probably 304 Not Modified");
+        }
+    }
+
+    /** override to prevent status update */
+    @Override
+    public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {}
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/NewsHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/NewsHandler.java
new file mode 100644
index 0000000000..135c254334
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/update/NewsHandler.java
@@ -0,0 +1,56 @@
+package net.i2p.router.update;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+import net.i2p.router.RouterContext;
+import net.i2p.router.web.ConfigUpdateHelper;
+import net.i2p.update.*;
+
+/**
+ * Task to periodically look for updates to the news.xml, and to keep
+ * track of whether that has an announcement for a new version.
+ *
+ * @since 0.9.2 moved from NewsFetcher
+ */
+class NewsHandler extends UpdateHandler {
+    
+    /** @since 0.7.14 not configurable */
+    private static final String BACKUP_NEWS_URL = "http://www.i2p2.i2p/_static/news/news.xml";
+
+    public NewsHandler(RouterContext ctx) {
+        super(ctx);
+    }
+
+    /**
+     *  This will check for news or router updates (it does the same thing).
+     *  Should not block.
+     *  @param currentVersion ignored, stored locally
+     */
+    public UpdateTask check(UpdateType type, UpdateMethod method,
+                            String id, String currentVersion, long maxTime) {
+        if ((type != UpdateType.ROUTER_SIGNED && type != UpdateType.ROUTER_SIGNED_PACK200 && type != UpdateType.NEWS) ||
+            method != UpdateMethod.HTTP)
+            return null;
+        List<URI> updateSources = new ArrayList(2);
+        try {
+            updateSources.add(new URI(ConfigUpdateHelper.getNewsURL(_context)));
+        } catch (URISyntaxException use) {}
+        try {
+            updateSources.add(new URI(BACKUP_NEWS_URL));
+        } catch (URISyntaxException use) {}
+        UpdateRunner update = new NewsFetcher(_context, updateSources);
+        update.start();
+        return update;
+    }
+
+    /**
+     *  Does nothing. check() also does update.
+     */
+    public UpdateTask update(UpdateType type, UpdateMethod method, List<URI> updateSources,
+                             String id, String newVersion, long maxTime) {
+        return null;
+    }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/NewsTimerTask.java b/apps/routerconsole/java/src/net/i2p/router/update/NewsTimerTask.java
new file mode 100644
index 0000000000..1ad65b3fcb
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/update/NewsTimerTask.java
@@ -0,0 +1,109 @@
+package net.i2p.router.update;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import net.i2p.crypto.TrustedUpdate;
+import net.i2p.data.DataHelper;
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.router.RouterVersion;
+import net.i2p.router.web.ConfigUpdateHandler;
+import net.i2p.router.web.ConfigUpdateHelper;
+import net.i2p.router.web.NewsHelper;
+import static net.i2p.update.UpdateType.*;
+import net.i2p.util.EepGet;
+import net.i2p.util.EepHead;
+import net.i2p.util.FileUtil;
+import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
+import net.i2p.util.SimpleTimer;
+
+/**
+ * Task to periodically look for updates to the news.xml, and to keep
+ * track of whether that has an announcement for a new version.
+ * Also looks for unsigned updates.
+ *
+ * Runs forever on instantiation, can't be stopped.
+ *
+ * @since 0.9.2 moved from NewsFetcher
+ */
+class NewsTimerTask implements SimpleTimer.TimedEvent {
+    private final RouterContext _context;
+    private final Log _log;
+    private final ConsoleUpdateManager _mgr;
+
+    private static final long INITIAL_DELAY = 5*60*1000;
+    private static final long RUN_DELAY = 10*60*1000;
+
+    public NewsTimerTask(RouterContext ctx) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(NewsTimerTask.class);
+        _mgr = (ConsoleUpdateManager) _context.updateManager();
+        ctx.simpleScheduler().addPeriodicEvent(this,
+                                             INITIAL_DELAY + _context.random().nextLong(INITIAL_DELAY),
+                                             RUN_DELAY);
+        // UpdateManager calls NewsFetcher to check the existing news at startup
+    }
+
+    public void timeReached() {
+        if (shouldFetchNews()) {
+            fetchNews();
+            if (shouldFetchUnsigned())
+                fetchUnsignedHead();
+        }
+    }
+    
+    private boolean shouldFetchNews() {
+        if (_context.router().gracefulShutdownInProgress())
+            return false;
+        if (NewsHelper.isUpdateInProgress())
+            return false;
+        long lastFetch = NewsHelper.lastChecked(_context);
+        String freq = _context.getProperty(ConfigUpdateHandler.PROP_REFRESH_FREQUENCY,
+                                           ConfigUpdateHandler.DEFAULT_REFRESH_FREQUENCY);
+        try {
+            long ms = Long.parseLong(freq);
+            if (ms <= 0)
+                return false;
+            
+            if (lastFetch + ms < _context.clock().now()) {
+                return true;
+            } else {
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("Last fetched " + DataHelper.formatDuration(_context.clock().now() - lastFetch) + " ago");
+                return false;
+            }
+        } catch (NumberFormatException nfe) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Invalid refresh frequency: " + freq);
+            return false;
+        }
+    }
+
+    private void fetchNews() {
+        _mgr.check(NEWS, "");
+    }
+    
+    private boolean shouldFetchUnsigned() {
+        String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
+        return url != null && url.length() > 0 &&
+               _context.getBooleanProperty(ConfigUpdateHandler.PROP_UPDATE_UNSIGNED) &&
+               !NewsHelper.dontInstall(_context);
+    }
+
+    /**
+     * HEAD the update url, and if the last-mod time is newer than the last update we
+     * downloaded, as stored in the properties, then we download it using eepget.
+     */
+    private void fetchUnsignedHead() {
+        _mgr.check(ROUTER_UNSIGNED, "");
+    }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateChecker.java b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateChecker.java
new file mode 100644
index 0000000000..acfe6626c6
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateChecker.java
@@ -0,0 +1,92 @@
+package net.i2p.router.update;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.net.URI;
+import java.util.List;
+import java.util.Properties;
+
+import net.i2p.crypto.TrustedUpdate;
+import net.i2p.router.RouterContext;
+import net.i2p.router.web.ConfigUpdateHandler;
+import net.i2p.update.*;
+import net.i2p.util.EepGet;
+import net.i2p.util.I2PAppThread;
+import net.i2p.util.PartialEepGet;
+import net.i2p.util.VersionComparator;
+
+/**
+ * Check for an updated version of a plugin.
+ * A plugin is a standard .sud file with a 40-byte signature,
+ * a 16-byte version, and a .zip file.
+ *
+ * So we get the current version and update URL for the installed plugin,
+ * then fetch the first 56 bytes of the URL, extract the version,
+ * and compare.
+ *
+ *  Moved from web/ and turned into an UpdateTask.
+ *
+ *  @since 0.7.12
+ */
+class PluginUpdateChecker extends UpdateRunner {
+    private final ByteArrayOutputStream _baos;
+    private final String _appName;
+    private final String _oldVersion;
+
+    public PluginUpdateChecker(RouterContext ctx, List<URI> uris, String appName, String oldVersion ) { 
+        super(ctx, uris);
+        _baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES);
+        if (!uris.isEmpty())
+            _currentURI = uris.get(0);
+        _appName = appName;
+        _oldVersion = oldVersion;
+    }
+
+
+    @Override
+    public UpdateType getType() { return UpdateType.PLUGIN; }
+
+        @Override
+        protected void update() {
+            // must be set for super
+            _isPartial = true;
+            updateStatus("<b>" + _("Checking for update of plugin {0}", _appName) + "</b>");
+            // use the same settings as for updater
+            // always proxy, or else FIXME
+            //boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
+            String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
+            int proxyPort = ConfigUpdateHandler.proxyPort(_context);
+            _baos.reset();
+            try {
+                _get = new PartialEepGet(_context, proxyHost, proxyPort, _baos, _currentURI.toString(), TrustedUpdate.HEADER_BYTES);
+                _get.addStatusListener(this);
+                _get.fetch();
+            } catch (Throwable t) {
+                _log.error("Error checking update for plugin", t);
+            }
+        }
+        
+        @Override
+        public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
+        }
+
+        @Override
+        public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
+            // super sets _newVersion if newer
+            boolean newer = _newVersion != null;
+            if (newer) {
+                _mgr.notifyVersionAvailable(this, _currentURI, UpdateType.PLUGIN, _appName, UpdateMethod.HTTP,
+                                            _urls, _newVersion, _oldVersion);
+            }
+            _mgr.notifyCheckComplete(this, newer, true);
+        }
+
+        @Override
+        public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
+            File f = new File(_updateFile);
+            f.delete();
+            _mgr.notifyCheckComplete(this, false, false);
+        }
+}
+    
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java
new file mode 100644
index 0000000000..fe394135b3
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java
@@ -0,0 +1,92 @@
+package net.i2p.router.update;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+
+import net.i2p.crypto.TrustedUpdate;
+import net.i2p.router.RouterContext;
+import net.i2p.router.web.PluginStarter;
+import net.i2p.update.*;
+import net.i2p.util.EepGet;
+import net.i2p.util.I2PAppThread;
+import net.i2p.util.PartialEepGet;
+import net.i2p.util.SimpleScheduler;
+import net.i2p.util.SimpleTimer;
+import net.i2p.util.VersionComparator;
+
+/**
+ * Check for or download an updated version of a plugin.
+ * A plugin is a standard .sud file with a 40-byte signature,
+ * a 16-byte version, and a .zip file.
+ *
+ * So we get the current version and update URL for the installed plugin,
+ * then fetch the first 56 bytes of the URL, extract the version,
+ * and compare.
+ *
+ * Moved from web/ and turned into an Updater.
+ *
+ * @since 0.7.12
+ * @author zzz
+ */
+class PluginUpdateHandler implements Updater {
+    private final RouterContext _context;
+
+    public PluginUpdateHandler(RouterContext ctx) {
+        _context = ctx;
+    }
+    
+    /** check a single plugin */
+    @Override
+    public UpdateTask check(UpdateType type, UpdateMethod method,
+                            String appName, String currentVersion, long maxTime) {
+        if ((type != UpdateType.PLUGIN) ||
+            method != UpdateMethod.HTTP || appName.length() <= 0)
+            return null;
+
+        Properties props = PluginStarter.pluginProperties(_context, appName);
+        String oldVersion = props.getProperty("version");
+        String xpi2pURL = props.getProperty("updateURL");
+        List<URI> updateSources = null;
+        if (xpi2pURL != null) {
+            try {
+                updateSources = Collections.singletonList(new URI(xpi2pURL));
+            } catch (URISyntaxException use) {}
+        }
+
+        if (oldVersion == null || updateSources == null) {
+            //updateStatus("<b>" + _("Cannot check, plugin {0} is not installed", appName) + "</b>");
+            return null;
+        }
+
+        UpdateRunner update = new PluginUpdateChecker(_context, updateSources, appName, oldVersion);
+        update.start();
+        return update;
+    }
+    
+    /** download a single plugin */
+    @Override
+    public UpdateTask update(UpdateType type, UpdateMethod method, List<URI> updateSources,
+                               String appName, String newVersion, long maxTime) {
+        if ((type != UpdateType.PLUGIN && type != UpdateType.PLUGIN_INSTALL) ||
+            method != UpdateMethod.HTTP || updateSources.isEmpty())
+            return null;
+        Properties props = PluginStarter.pluginProperties(_context, appName);
+        String oldVersion = props.getProperty("version");
+        String xpi2pURL = props.getProperty("updateURL");
+        if (oldVersion == null || xpi2pURL == null) {
+            //updateStatus("<b>" + _("Cannot check, plugin {0} is not installed", appName) + "</b>");
+            return null;
+        }
+
+        UpdateRunner update = new PluginUpdateRunner(_context, updateSources, appName, oldVersion);
+        update.start();
+        return update;
+    }
+}
+    
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java
similarity index 82%
rename from apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java
rename to apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java
index ce60170361..fbac990d4d 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java
@@ -1,7 +1,9 @@
-package net.i2p.router.web;
+package net.i2p.router.update;
 
 import java.io.File;
 import java.io.IOException;
+import java.net.URI;
+import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 
@@ -9,6 +11,12 @@ import net.i2p.CoreVersion;
 import net.i2p.crypto.TrustedUpdate;
 import net.i2p.data.DataHelper;
 import net.i2p.router.RouterContext;
+import net.i2p.router.web.ConfigClientsHelper;
+import net.i2p.router.web.ConfigUpdateHandler;
+import net.i2p.router.web.LogsHelper;
+import net.i2p.router.web.Messages;
+import net.i2p.router.web.PluginStarter;
+import net.i2p.update.*;
 import net.i2p.util.EepGet;
 import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
@@ -19,107 +27,55 @@ import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 import net.i2p.util.VersionComparator;
 
+
 /**
- * Download and install a plugin.
+ * Check for an updated version of a plugin.
  * A plugin is a standard .sud file with a 40-byte signature,
  * a 16-byte version, and a .zip file.
- * Unlike for router updates, we need not have the public key
- * for the signature in advance.
  *
- * The zip file must have a standard directory layout, with
- * a plugin.config file at the top level.
- * The config file contains properties for the package name, version,
- * signing public key, and other settings.
- * The zip file will typically contain a webapps/ or lib/ dir,
- * and a webapps.config and/or clients.config file.
+ * So we get the current version and update URL for the installed plugin,
+ * then fetch the first 56 bytes of the URL, extract the version,
+ * and compare.
+ *
+ * Moved from web/ and turned into an UpdateTask.
  *
- * @since 0.7.12
- * @author zzz
+ * @since 0.9.2 moved from PluginUpdateHandler
  */
-public class PluginUpdateHandler extends UpdateHandler {
-    private static PluginUpdateRunner _pluginUpdateRunner;
-    private String _xpi2pURL;
-    private String _appStatus;
-    private volatile boolean _updated;
+class PluginUpdateRunner extends UpdateRunner {
+
+    private String _appName;
+    private final String _oldVersion;
+    private final URI _uri;
+    private final String _xpi2pURL;
+    private boolean _updated;
 
     private static final String XPI2P = "app.xpi2p";
     private static final String ZIP = XPI2P + ".zip";
-    public static final String PLUGIN_DIR = "plugins";
-
-    private static PluginUpdateHandler _instance;
-    public static final synchronized PluginUpdateHandler getInstance(RouterContext ctx) {
-        if (_instance != null)
-            return _instance;
-        _instance = new PluginUpdateHandler(ctx);
-        return _instance;
+    public static final String PLUGIN_DIR = PluginStarter.PLUGIN_DIR;
+
+    public PluginUpdateRunner(RouterContext ctx, List<URI> uris, String appName, String oldVersion ) { 
+        super(ctx, uris);
+        if (uris.isEmpty())
+            _uri = null;
+        else
+            _uri = uris.get(0);
+        _xpi2pURL = _uri.toString();
+        _appName = appName;
+        _oldVersion = oldVersion;
     }
 
-    private PluginUpdateHandler(RouterContext ctx) {
-        super(ctx);
-        _appStatus = "";
-    }
-
-    public void update(String xpi2pURL) {
-        // don't block waiting for the other one to finish
-        if ("true".equals(System.getProperty(PROP_UPDATE_IN_PROGRESS))) {
-            _log.error("Update already running");
-            return;
-        }
-        synchronized (UpdateHandler.class) {
-            if (_pluginUpdateRunner == null)
-                _pluginUpdateRunner = new PluginUpdateRunner(_xpi2pURL);
-            if (_pluginUpdateRunner.isRunning())
-                return;
-            _xpi2pURL = xpi2pURL;
-            _updateFile = (new File(_context.getTempDir(), "tmp" + _context.random().nextInt() + XPI2P)).getAbsolutePath();
-            System.setProperty(PROP_UPDATE_IN_PROGRESS, "true");
-            I2PAppThread update = new I2PAppThread(_pluginUpdateRunner, "AppDownload");
-            update.start();
-        }
-    }
-
-    public String getAppStatus() {
-        return _appStatus;
-    }
-
-    public boolean isRunning() {
-        return _pluginUpdateRunner != null && _pluginUpdateRunner.isRunning();
-    }
 
     @Override
-    public boolean isDone() {
-        // FIXME
-        return false;
-    }
-
-    /** @since 0.8.13 */
-    public boolean wasUpdateSuccessful() {
-        return _updated;
-    }
-
-    private void scheduleStatusClean(String msg) {
-        SimpleScheduler.getInstance().addEvent(new Cleaner(msg), 20*60*1000);
-    }
-
-    private class Cleaner implements SimpleTimer.TimedEvent {
-        private final String _msg;
-        public Cleaner(String msg) {
-            _msg = msg;
-        }
-        public void timeReached() {
-            if (_msg.equals(getStatus()))
-                updateStatus("");
-        }
+    public UpdateType getType() {
+        return _oldVersion.equals("") ? UpdateType.PLUGIN_INSTALL : UpdateType.PLUGIN;
     }
 
-    public class PluginUpdateRunner extends UpdateRunner implements Runnable, EepGet.StatusListener {
-
-        public PluginUpdateRunner(String url) {
-            super();
-        }
+    @Override
+    public URI getURI() { return _uri; }
 
         @Override
         protected void update() {
+
             _updated = false;
             if(_xpi2pURL.startsWith("file://")) {
                 updateStatus("<b>" + _("Attempting to install from file {0}", _xpi2pURL) + "</b>");
@@ -156,21 +112,6 @@ public class PluginUpdateHandler extends UpdateHandler {
             }
         }
 
-        @Override
-        public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
-            StringBuilder buf = new StringBuilder(64);
-            buf.append("<b>").append(_("Downloading plugin")).append(' ');
-            double pct = ((double)alreadyTransferred + (double)currentWrite) /
-                         ((double)alreadyTransferred + (double)currentWrite + bytesRemaining);
-            synchronized (_pct) {
-                buf.append(_pct.format(pct));
-            }
-            buf.append(": ");
-            buf.append(_("{0}B transferred", DataHelper.formatSize2(currentWrite + alreadyTransferred)));
-            buf.append("</b>");
-            updateStatus(buf.toString());
-        }
-
         @Override
         public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
             boolean update = false;
@@ -468,15 +409,7 @@ public class PluginUpdateHandler extends UpdateHandler {
 
         private void statusDone(String msg) {
             updateStatus(msg);
-            scheduleStatusClean(msg);
         }
 
-    }
-
-    @Override
-    protected void updateStatus(String s) {
-        super.updateStatus(s);
-        _appStatus = s;
-    }
 }
 
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateChecker.java b/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateChecker.java
new file mode 100644
index 0000000000..f56979e36c
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateChecker.java
@@ -0,0 +1,94 @@
+package net.i2p.router.update;
+
+import java.io.File;
+import java.net.URI;
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Date;
+
+import net.i2p.router.RouterContext;
+import net.i2p.router.util.RFC822Date;
+import net.i2p.router.web.ConfigUpdateHandler;
+import net.i2p.router.web.Messages;
+import net.i2p.update.*;
+import net.i2p.util.EepHead;
+import net.i2p.util.I2PAppThread;
+import net.i2p.util.Log;
+
+/**
+ *  Does a simple EepHead to get the last-modified header.
+ *  Moved from NewsFetcher and turned into an UpdateTask.
+ *
+ *  Overrides UpdateRunner for convenience, does not use super's Eepget StatusListener
+ *
+ *  @since 0.9.2
+ */
+class UnsignedUpdateChecker extends UpdateRunner {
+    private final long _ms;
+    private boolean _unsignedUpdateAvailable;
+
+    protected static final String SIGNED_UPDATE_FILE = "i2pupdate.sud";
+
+    public UnsignedUpdateChecker(RouterContext ctx, List<URI> uris, long lastUpdateTime) { 
+        super(ctx, uris);
+        _ms = lastUpdateTime;
+    }
+
+    //////// begin UpdateTask methods
+
+    @Override
+    public UpdateType getType() { return UpdateType.ROUTER_UNSIGNED; }
+
+    //////// end UpdateTask methods
+
+    @Override
+    public void run() {
+        _isRunning = true;
+        boolean success = false;
+        try {
+            success = fetchUnsignedHead();
+        } finally {
+            _isRunning = false;
+        }
+        _mgr.notifyCheckComplete(this, _unsignedUpdateAvailable, success);
+    }
+
+
+    /**
+     * HEAD the update url, and if the last-mod time is newer than the last update we
+     * downloaded, as stored in the properties, then we download it using eepget.
+     */
+    private boolean fetchUnsignedHead() {
+        if (_urls.isEmpty())
+            return false;
+        _currentURI = _urls.get(0);
+        String url = _currentURI.toString();
+        // assume always proxied for now
+        //boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
+        String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
+        int proxyPort = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_PORT, ConfigUpdateHandler.DEFAULT_PROXY_PORT_INT);
+
+        try {
+            EepHead get = new EepHead(_context, proxyHost, proxyPort, 0, url);
+            if (get.fetch()) {
+                String lastmod = get.getLastModified();
+                if (lastmod != null) {
+                    long modtime = RFC822Date.parse822Date(lastmod);
+                    if (modtime <= 0) return false;
+                    if (_ms <= 0) return false;
+                    if (modtime > _ms) {
+                        _unsignedUpdateAvailable = true;
+                        // '07-Jul 21:09 UTC' with month name in the system locale
+                        String unsignedUpdateVersion = (new SimpleDateFormat("dd-MMM HH:mm")).format(new Date(modtime)) + " UTC";
+                        _mgr.notifyVersionAvailable(this, _urls.get(0), getType(), "", getMethod(), _urls,
+                                                    unsignedUpdateVersion, "");
+                    }
+                }
+                return true;
+            }
+        } catch (Throwable t) {
+            _log.error("Error fetching the unsigned update", t);
+        }
+        return false;
+    }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateHandler.java
new file mode 100644
index 0000000000..7e5628684a
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateHandler.java
@@ -0,0 +1,99 @@
+package net.i2p.router.update;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.List;
+
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.router.util.RFC822Date;
+import net.i2p.router.web.ConfigUpdateHandler;
+import net.i2p.router.web.NewsHelper;
+import net.i2p.update.*;
+import net.i2p.util.EepGet;
+import net.i2p.util.FileUtil;
+import net.i2p.util.I2PAppThread;
+import net.i2p.util.Log;
+
+/**
+ * <p>Handles the request to update the router by firing off an
+ * {@link net.i2p.util.EepGet} call to download the latest unsigned zip file
+ * and displaying the status to anyone who asks.
+ * </p>
+ * <p>After the download completes the signed update file is copied to the
+ * router directory, and if configured the router is restarted to complete
+ * the update process.
+ * </p>
+ */
+class UnsignedUpdateHandler implements Updater {
+    private final RouterContext _context;
+
+    public UnsignedUpdateHandler(RouterContext ctx) {
+        _context = ctx;
+    }
+
+    /**
+     *  @param currentVersion ignored, we use time stored in a property
+     */
+    @Override
+    public UpdateTask check(UpdateType type, UpdateMethod method,
+                            String id, String currentVersion, long maxTime) {
+        if (type != UpdateType.ROUTER_UNSIGNED || method != UpdateMethod.HTTP)
+            return null;
+
+        String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
+        if (url == null)
+            return null;
+
+        List<URI> updateSources;
+        try {
+            updateSources = Collections.singletonList(new URI(url));
+        } catch (URISyntaxException use) {
+            return null;
+        }
+
+        String lastUpdate = _context.getProperty(NewsHelper.PROP_LAST_UPDATE_TIME);
+        if (lastUpdate == null) {
+            // we don't know what version you have, so stamp it with the current time,
+            // and we'll look for something newer next time around.
+            _context.router().saveConfig(NewsHelper.PROP_LAST_UPDATE_TIME,
+                                               Long.toString(_context.clock().now()));
+            return null;
+        }
+        long ms = 0;
+        try {
+            ms = Long.parseLong(lastUpdate);
+        } catch (NumberFormatException nfe) {}
+        if (ms <= 0) {
+            // we don't know what version you have, so stamp it with the current time,
+            // and we'll look for something newer next time around.
+            _context.router().saveConfig(NewsHelper.PROP_LAST_UPDATE_TIME,
+                                               Long.toString(_context.clock().now()));
+            return null;
+        }
+
+        UpdateRunner update = new UnsignedUpdateChecker(_context, updateSources, ms);
+        update.start();
+        return update;
+    }
+
+    /**
+     *  Start a download and return a handle to the download task.
+     *  Should not block.
+     *
+     *  @param id plugin name or ignored
+     *  @param maxTime how long you have
+     *  @return active task or null if unable to download
+     */
+    @Override
+    public UpdateTask update(UpdateType type, UpdateMethod method, List<URI> updateSources,
+                             String id, String newVersion, long maxTime) {
+        if (type != UpdateType.ROUTER_UNSIGNED || method != UpdateMethod.HTTP || updateSources.isEmpty())
+            return null;
+        UpdateRunner update = new UnsignedUpdateRunner(_context, updateSources);
+        update.start();
+        return update;
+    }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateRunner.java b/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateRunner.java
new file mode 100644
index 0000000000..57cc9d2e1e
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateRunner.java
@@ -0,0 +1,69 @@
+package net.i2p.router.update;
+
+import java.io.File;
+import java.net.URI;
+import java.util.List;
+
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.router.util.RFC822Date;
+import net.i2p.router.web.ConfigUpdateHandler;
+import net.i2p.update.*;
+import net.i2p.util.EepGet;
+import net.i2p.util.FileUtil;
+import net.i2p.util.I2PAppThread;
+import net.i2p.util.Log;
+
+    
+/**
+ *  Eepget the .zip file to the temp dir, then notify.r
+ *  Moved from UnsignedUpdateHandler and turned into an UpdateTask.
+ *
+ *  @since 0.9.2
+ */
+class UnsignedUpdateRunner extends UpdateRunner {
+
+    public UnsignedUpdateRunner(RouterContext ctx, List<URI> uris) { 
+        super(ctx, uris);
+        if (!uris.isEmpty())
+            _currentURI = uris.get(0);
+    }
+
+
+    @Override
+    public UpdateType getType() { return UpdateType.ROUTER_UNSIGNED; }
+
+
+        /** Get the file */
+        @Override
+        protected void update() {
+            String zipURL = _currentURI.toString();
+            updateStatus("<b>" + _("Updating") + "</b>");
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Starting unsigned update URL: " + zipURL);
+            // always proxy for now
+            //boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
+            String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
+            int proxyPort = ConfigUpdateHandler.proxyPort(_context);
+            try {
+                // 40 retries!!
+                _get = new EepGet(_context, proxyHost, proxyPort, 40, _updateFile, zipURL, false);
+                _get.addStatusListener(UnsignedUpdateRunner.this);
+                _get.fetch();
+            } catch (Throwable t) {
+                _log.error("Error updating", t);
+            }
+        }
+        
+        /** eepget listener callback Overrides */
+        @Override
+        public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
+            String lastmod = _get.getLastModified();
+            File tmp = new File(_updateFile);
+/////// FIXME RFC822 or long?
+            if (_mgr.notifyComplete(this, lastmod, tmp))
+                this.done = true;
+            else
+                tmp.delete();  // corrupt
+        }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/UpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/UpdateHandler.java
new file mode 100644
index 0000000000..9c79dcb282
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/update/UpdateHandler.java
@@ -0,0 +1,69 @@
+package net.i2p.router.update;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.net.URI;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import net.i2p.crypto.TrustedUpdate;
+import net.i2p.data.DataHelper;
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.router.RouterVersion;
+import net.i2p.router.util.RFC822Date;
+import net.i2p.update.*;
+import net.i2p.util.EepGet;
+import net.i2p.util.I2PAppThread;
+import net.i2p.util.Log;
+import net.i2p.util.PartialEepGet;
+import net.i2p.util.VersionComparator;
+
+/**
+ * <p>Handles the request to update the router by firing one or more
+ * {@link net.i2p.util.EepGet} calls to download the latest signed update file
+ * and displaying the status to anyone who asks.
+ * </p>
+ * <p>After the download completes the signed update file is verified with
+ * {@link net.i2p.crypto.TrustedUpdate}, and if it's authentic the payload
+ * of the signed update file is unpacked and the router is restarted to complete
+ * the update process.
+ * </p>
+ *
+ * This does not do any checking, that is handled by the NewsFetcher.
+ */
+class UpdateHandler implements Updater {
+    protected final RouterContext _context;
+    
+    public UpdateHandler(RouterContext ctx) {
+        _context = ctx;
+    }
+    
+    /** Can't check, the NewsHandler does that */
+    public UpdateTask check(UpdateType type, UpdateMethod method,
+                            String id, String currentVersion, long maxTime) {
+        return null;
+    }
+
+    /**
+     *  Start a download and return a handle to the download task.
+     *  Should not block.
+     *
+     *  @param id plugin name or ignored
+     *  @param maxTime how long you have
+     *  @return active task or null if unable to download
+     */
+    public UpdateTask update(UpdateType type, UpdateMethod method, List<URI> updateSources,
+                             String id, String newVersion, long maxTime) {
+        if ((type != UpdateType.ROUTER_SIGNED && type != UpdateType.ROUTER_SIGNED_PACK200) ||
+            method != UpdateMethod.HTTP || updateSources.isEmpty())
+            return null;
+        UpdateRunner update = new UpdateRunner(_context, updateSources);
+        update.start();
+        return update;
+    }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java b/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java
new file mode 100644
index 0000000000..52e77ba95b
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java
@@ -0,0 +1,231 @@
+package net.i2p.router.update;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.net.URI;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import net.i2p.crypto.TrustedUpdate;
+import net.i2p.data.DataHelper;
+import net.i2p.router.RouterContext;
+import net.i2p.router.RouterVersion;
+import net.i2p.router.web.ConfigUpdateHandler;
+import net.i2p.router.web.Messages;
+import net.i2p.update.*;
+import net.i2p.util.EepGet;
+import net.i2p.util.I2PAppThread;
+import net.i2p.util.Log;
+import net.i2p.util.PartialEepGet;
+import net.i2p.util.VersionComparator;
+
+/**
+ *  The downloader for router signed updates,
+ *  and the base class for all the other Checkers and Runners.
+ *
+ *  @since 0.9.2 moved from UpdateHandler
+ *
+ */
+class UpdateRunner extends I2PAppThread implements UpdateTask, EepGet.StatusListener {
+    protected final RouterContext _context;
+    protected final Log _log;
+    protected final ConsoleUpdateManager _mgr;
+    protected final List<URI> _urls;
+    protected final String _updateFile;
+    protected volatile boolean _isRunning;
+    protected boolean done;
+    protected EepGet _get;
+    /** tells the listeners what mode we are in - set to true in extending classes for checks */
+    protected boolean _isPartial;
+    /** set by the listeners on completion */
+    protected String _newVersion;
+    private ByteArrayOutputStream _baos;
+    protected URI _currentURI;
+
+    private static final String SIGNED_UPDATE_FILE = "i2pupdate.sud";
+
+    public UpdateRunner(RouterContext ctx, List<URI> uris) { 
+        super("Update Runner");
+        setDaemon(true);
+        _context = ctx;
+        _log = ctx.logManager().getLog(getClass());
+        _mgr = (ConsoleUpdateManager) ctx.updateManager();
+        _urls = uris;
+        _updateFile = (new File(ctx.getTempDir(), "update" + ctx.random().nextInt() + ".tmp")).getAbsolutePath();
+    }
+
+    //////// begin UpdateTask methods
+
+    public boolean isRunning() { return _isRunning; }
+
+    public void shutdown() {
+        _isRunning = false;
+        interrupt();
+    }
+
+    public UpdateType getType() { return UpdateType.ROUTER_SIGNED; }
+
+    public UpdateMethod getMethod() { return UpdateMethod.HTTP; }
+
+    public URI getURI() { return _currentURI; }
+
+    public String getID() { return ""; }
+
+    //////// end UpdateTask methods
+
+    @Override
+    public void run() {
+        _isRunning = true;
+        try {
+            update();
+        } finally {
+            _isRunning = false;
+        }
+    }
+
+    /**
+     *  Loop through the entire list of update URLs.
+     *  For each one, first get the version from the first 56 bytes and see if
+     *  it is newer than what we are running now.
+     *  If it is, get the whole thing.
+     */
+    protected void update() {
+        // Do a PartialEepGet on the selected URL, check for version we expect,
+        // and loop if it isn't what we want.
+        // This will allows us to do a release without waiting for the last host to install the update.
+        // Alternative: In bytesTransferred(), Check the data in the output file after
+        // we've received at least 56 bytes. Need a cancel() method in EepGet ?
+
+        boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
+        String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
+        int proxyPort = ConfigUpdateHandler.proxyPort(_context);
+
+        if (_urls.isEmpty()) {
+            // not likely, don't bother translating
+            updateStatus("<b>Update source list is empty, cannot download update</b>");
+            _log.log(Log.CRIT, "Update source list is empty - cannot download update");
+            _mgr.notifyTaskFailed(this, "", null);
+            return;
+        }
+
+        ByteArrayOutputStream baos = null;
+        if (shouldProxy)
+            baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES);
+        for (URI uri : _urls) {
+            _currentURI = uri;
+            String updateURL = uri.toString();
+            updateStatus("<b>" + _("Updating from {0}", linkify(updateURL)) + "</b>");
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Selected update URL: " + updateURL);
+
+            // Check the first 56 bytes for the version
+            if (shouldProxy) {
+                _isPartial = true;
+                baos.reset();
+                try {
+                    // no retries
+                    _get = new PartialEepGet(_context, proxyHost, proxyPort, baos, updateURL, TrustedUpdate.HEADER_BYTES);
+                    _get.addStatusListener(UpdateRunner.this);
+                    _get.fetch();
+                } catch (Throwable t) {
+                }
+                _isPartial = false;
+                if (_newVersion == null)
+                    continue;
+            }
+
+            // Now get the whole thing
+            try {
+                if (shouldProxy)
+                    // 40 retries!!
+                    _get = new EepGet(_context, proxyHost, proxyPort, 40, _updateFile, updateURL, false);
+                else
+                    _get = new EepGet(_context, 1, _updateFile, updateURL, false);
+                _get.addStatusListener(UpdateRunner.this);
+                _get.fetch();
+            } catch (Throwable t) {
+                _log.error("Error updating", t);
+            }
+            if (this.done)
+                break;
+        }
+        (new File(_updateFile)).delete();
+        if (!this.done)
+            _mgr.notifyTaskFailed(this, "", null);
+    }
+    
+    // EepGet Listeners below.
+    // We use the same for both the partial and the full EepGet,
+    // with a couple of adjustments depending on which mode.
+
+    public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) {
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Attempt failed on " + url, cause);
+        // ignored
+    }
+
+    /** subclasses should override */
+    public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
+        if (_isPartial)
+            return;
+        long d = currentWrite + bytesTransferred;
+        String status = "<b>" + _("Updating") + "</b>";
+        _mgr.notifyProgress(this, status, d, d + bytesRemaining);
+    }
+
+    /** subclasses should override */
+    public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
+        if (_isPartial) {
+            // Compare version with what we have now
+            String newVersion = TrustedUpdate.getVersionString(new ByteArrayInputStream(_baos.toByteArray()));
+            boolean newer = (new VersionComparator()).compare(newVersion, RouterVersion.VERSION) > 0;
+            if (newer) {
+                _newVersion = newVersion;
+            } else {
+                updateStatus("<b>" + _("No new version found at {0}", linkify(url)) + "</b>");
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Found old version \"" + newVersion + "\" at " + url);
+            }
+            return;
+        }
+
+        File tmp = new File(_updateFile);
+        if (_mgr.notifyComplete(this, _newVersion, tmp))
+            this.done = true;
+        else
+            tmp.delete();  // corrupt
+    }
+
+    /** subclasses should override */
+    public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
+        // don't display bytesTransferred as it is meaningless
+        _log.error("Update from " + url + " did not download completely (" +
+                           bytesRemaining + " remaining after " + currentAttempt + " tries)");
+        updateStatus("<b>" + _("Transfer failed from {0}", linkify(url)) + "</b>");
+    }
+
+    public void headerReceived(String url, int attemptNum, String key, String val) {}
+
+    public void attempting(String url) {}
+
+    protected void updateStatus(String s) {
+        _mgr.notifyProgress(this, s);
+    }
+
+    protected static String linkify(String url) {
+        return ConsoleUpdateManager.linkify(url);
+    }
+
+    /** translate a string */
+    protected String _(String s) {
+        return Messages.getString(s, _context);
+    }
+
+    /**
+     *  translate a string with a parameter
+     */
+    protected String _(String s, Object o) {
+        return Messages.getString(s, o, _context);
+    }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/package.html b/apps/routerconsole/java/src/net/i2p/router/update/package.html
new file mode 100644
index 0000000000..5b03299139
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/update/package.html
@@ -0,0 +1,7 @@
+<html>
+<body>
+<p>
+Classes to implement the update process.
+</p>
+</body>
+</html>
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/CSSHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/CSSHelper.java
index 3dd724b08d..4e0921cb4b 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/CSSHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/CSSHelper.java
@@ -56,7 +56,7 @@ public class CSSHelper extends HelperBase {
     public void setNews(String val) {
         // Protected with nonce in css.jsi
         if (val != null)
-            NewsFetcher.getInstance(_context).showNews(val.equals("1"));
+            NewsHelper.showNews(_context, val.equals("1"));
     }
 
     /**
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java
index 6c37db8290..06f87e377f 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java
@@ -1,6 +1,8 @@
 package net.i2p.router.web;
 
 import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -12,6 +14,8 @@ import java.util.Set;
 import net.i2p.router.client.ClientManagerFacadeImpl;
 import net.i2p.router.startup.ClientAppConfig;
 import net.i2p.router.startup.LoadClientAppsJob;
+import net.i2p.router.update.ConsoleUpdateManager;
+import static net.i2p.update.UpdateType.*;
 
 import org.mortbay.jetty.handler.ContextHandlerCollection;
 
@@ -333,7 +337,7 @@ public class ConfigClientsHandler extends FormHandler {
 
     /** @since 0.8.13 */
     private void updateAllPlugins() {
-        if ("true".equals(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS))) {
+        if (NewsHelper.isAnyUpdateInProgress()) {
             addFormError(_("Plugin or update download already in progress."));
             return;
         }
@@ -346,17 +350,26 @@ public class ConfigClientsHandler extends FormHandler {
     }
 
     private void installPlugin(String url) {
-        if ("true".equals(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS))) {
-            addFormError(_("Plugin or update download already in progress."));
+        ConsoleUpdateManager mgr = (ConsoleUpdateManager) _context.updateManager();
+        if (mgr == null) {
+            addFormError("Update manager not registered, cannot install");
             return;
         }
-        PluginUpdateHandler puh = PluginUpdateHandler.getInstance(_context);
-        if (puh.isRunning()) {
+        if (mgr.isUpdateInProgress()) {
             addFormError(_("Plugin or update download already in progress."));
             return;
         }
-        puh.update(url);
-        addFormNotice(_("Downloading plugin from {0}", url));
+        URI uri;
+        try {
+            uri = new URI(url);
+        } catch (URISyntaxException use) {
+            addFormError(_("Bad URL {0}", url));
+            return;
+        }
+        if (mgr.installPlugin(uri))
+            addFormNotice(_("Downloading plugin from {0}", url));
+        else
+            addFormError("Cannot install, check logs");
         // So that update() will post a status to the summary bar before we reload
         try {
            Thread.sleep(1000);
@@ -364,16 +377,12 @@ public class ConfigClientsHandler extends FormHandler {
     }
 
     private void checkPlugin(String app) {
-        if ("true".equals(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS))) {
-            addFormError(_("Plugin or update download already in progress."));
-            return;
-        }
-        PluginUpdateChecker puc = PluginUpdateChecker.getInstance(_context);
-        if (puc.isRunning()) {
-            addFormError(_("Plugin or update download already in progress."));
+        ConsoleUpdateManager mgr = (ConsoleUpdateManager) _context.updateManager();
+        if (mgr == null) {
+            addFormError("Update manager not registered, cannot check");
             return;
         }
-        puc.update(app);
+        mgr.check(PLUGIN, app);
         addFormNotice(_("Checking plugin {0} for updates", app));
         // So that update() will post a status to the summary bar before we reload
         try {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java
index f94f4b9c77..97f30eeee5 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java
@@ -293,7 +293,7 @@ public class ConfigClientsHelper extends HelperBase {
      *  Like in DataHelper but doesn't convert null to ""
      *  There's a lot worse things a plugin could do but...
      */
-    static String stripHTML(Properties props, String key) {
+    public static String stripHTML(Properties props, String key) {
         String orig = props.getProperty(key);
         if (orig == null) return null;
         String t1 = orig.replace('<', ' ');
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java
index a3b30d337a..0340dde3e7 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java
@@ -6,6 +6,8 @@ import java.util.Map;
 import net.i2p.I2PAppContext;
 import net.i2p.crypto.TrustedUpdate;
 import net.i2p.data.DataHelper;
+import net.i2p.router.update.ConsoleUpdateManager;
+import static net.i2p.update.UpdateType.*;
 import net.i2p.util.FileUtil;
 import net.i2p.util.PortMapper;
 
@@ -84,7 +86,7 @@ public class ConfigUpdateHandler extends FormHandler {
      *  @return the configured value, else the registered HTTP proxy, else the default
      *  @since 0.8.13
      */
-    static int proxyPort(I2PAppContext ctx) {
+    public static int proxyPort(I2PAppContext ctx) {
         return ctx.getProperty(PROP_PROXY_PORT,
                                ctx.portMapper().getPort(PortMapper.SVC_HTTP_PROXY, DEFAULT_PROXY_PORT_INT));
     }
@@ -94,11 +96,16 @@ public class ConfigUpdateHandler extends FormHandler {
         if (_action == null)
             return;
         if (_action.equals(_("Check for updates"))) {
-            NewsFetcher fetcher = NewsFetcher.getInstance(_context);
-            fetcher.fetchNews();
-            if (fetcher.shouldFetchUnsigned())
-                fetcher.fetchUnsignedHead();
-            if (fetcher.updateAvailable() || fetcher.unsignedUpdateAvailable()) {
+            ConsoleUpdateManager mgr = (ConsoleUpdateManager) _context.updateManager();
+            if (mgr == null) {
+                addFormError("Update manager not registered, cannot check");
+                return;
+            }
+            boolean a1 = mgr.checkAvailable(NEWS, 60*1000) != null;
+            boolean a2 = false;
+            if ((!a1) && _updateUnsigned && _zipURL != null && _zipURL.length() > 0)
+                a2 = mgr.checkAvailable(ROUTER_UNSIGNED, 60*1000) != null;
+            if (a1 || a2) {
                 if ( (_updatePolicy == null) || (!_updatePolicy.equals("notify")) )
                     addFormNotice(_("Update available, attempting to download now"));
                 else
@@ -118,7 +125,8 @@ public class ConfigUpdateHandler extends FormHandler {
             String oldURL = ConfigUpdateHelper.getNewsURL(_context);
             if ( (oldURL == null) || (!_newsURL.equals(oldURL)) ) {
                 changes.put(PROP_NEWS_URL, _newsURL);
-                NewsFetcher.getInstance(_context).invalidateNews();
+                // this invalidates the news
+                changes.put(NewsHelper.PROP_LAST_CHECKED, "0");
                 addFormNotice(_("Updating news URL to {0}", _newsURL));
             }
         }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java
index 8ef110c396..4203d442b9 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java
@@ -14,7 +14,7 @@ public class ConfigUpdateHelper extends HelperBase {
     @Override
     public void setContextId(String contextId) {
         super.setContextId(contextId);
-        _dontInstall = NewsFetcher.getInstance(_context).dontInstall();
+        _dontInstall = NewsHelper.dontInstall(_context);
     }
 
     public boolean canInstall() {
@@ -160,6 +160,6 @@ public class ConfigUpdateHelper extends HelperBase {
     }
 
     public String getNewsStatus() { 
-        return NewsFetcher.getInstance(_context).status();
+        return NewsHelper.status(_context);
     }
 }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/FileDumpHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/FileDumpHelper.java
index 7bb1e99d05..41ffec627b 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/FileDumpHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/FileDumpHelper.java
@@ -60,7 +60,7 @@ public class FileDumpHelper extends HelperBase {
         dumpDir(buf, dir, ".war");
 
         // plugins
-        File pluginDir = new File(_context.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR);
+        File pluginDir = new File(_context.getConfigDir(), PluginStarter.PLUGIN_DIR);
         File[] files = pluginDir.listFiles();
         if (files != null) {
             Arrays.sort(files);
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
index aa6596a204..e29cc3930e 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
@@ -20,7 +20,7 @@ public class LogsHelper extends HelperBase {
     }
 
     /** @since 0.8.13 */
-    static String jettyVersion() {
+    public static String jettyVersion() {
         return Server.getVersion();
     }
 
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java b/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java
deleted file mode 100644
index 23210dced0..0000000000
--- a/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java
+++ /dev/null
@@ -1,416 +0,0 @@
-package net.i2p.router.web;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-
-import net.i2p.crypto.TrustedUpdate;
-import net.i2p.data.DataHelper;
-import net.i2p.router.Router;
-import net.i2p.router.RouterContext;
-import net.i2p.router.RouterVersion;
-import net.i2p.router.util.RFC822Date;
-import net.i2p.util.EepGet;
-import net.i2p.util.EepHead;
-import net.i2p.util.FileUtil;
-import net.i2p.util.Log;
-
-/**
- * Task to periodically look for updates to the news.xml, and to keep
- * track of whether that has an announcement for a new version.
- */
-public class NewsFetcher implements Runnable, EepGet.StatusListener {
-    private final RouterContext _context;
-    private final Log _log;
-    private boolean _updateAvailable;
-    private boolean _unsignedUpdateAvailable;
-    private long _lastFetch;
-    private long _lastUpdated;
-    private String _updateVersion;
-    private String _unsignedUpdateVersion;
-    private String _lastModified;
-    private boolean _invalidated;
-    private final File _newsFile;
-    private final File _tempFile;
-    private static NewsFetcher _instance;
-    private volatile boolean _isRunning;
-
-    //public static final synchronized NewsFetcher getInstance() { return _instance; }
-    public static final synchronized NewsFetcher getInstance(RouterContext ctx) { 
-        if (_instance != null)
-            return _instance;
-        _instance = new NewsFetcher(ctx);
-        return _instance;
-    }
-
-    private static final String NEWS_FILE = "docs/news.xml";
-    private static final String TEMP_NEWS_FILE = "news.xml.temp";
-    /** @since 0.7.14 not configurable */
-    private static final String BACKUP_NEWS_URL = "http://www.i2p2.i2p/_static/news/news.xml";
-    private static final String PROP_LAST_CHECKED = "router.newsLastChecked";
-    /** @since 0.8.12 */
-    private static final String PROP_LAST_HIDDEN = "routerconsole.newsLastHidden";
-    
-    private NewsFetcher(RouterContext ctx) {
-        _context = ctx;
-        _log = ctx.logManager().getLog(NewsFetcher.class);
-        _instance = this;
-        try {
-            String last = ctx.getProperty(PROP_LAST_CHECKED);
-            if (last != null)
-                _lastFetch = Long.parseLong(last);
-        } catch (NumberFormatException nfe) {}
-        _newsFile = new File(_context.getRouterDir(), NEWS_FILE);
-        _tempFile = new File(_context.getTempDir(), TEMP_NEWS_FILE);
-        updateLastFetched();
-        _updateVersion = "";
-        _isRunning = true;
-    }
-    
-    /** @since 0.8.8 */
-    void shutdown() {
-        _isRunning = false;
-    }
-
-    private void updateLastFetched() {
-        if (_newsFile.exists()) {
-            if (_lastUpdated == 0)
-                _lastUpdated = _newsFile.lastModified();
-            if (_lastFetch == 0)
-                _lastFetch = _lastUpdated;
-            if (_lastModified == null)
-                _lastModified = RFC822Date.to822Date(_lastFetch);
-        } else {
-            _lastUpdated = 0;
-            _lastFetch = 0;
-            _lastModified = null;
-        }
-    }
-    
-    public boolean updateAvailable() { return _updateAvailable; }
-    public String updateVersion() { return _updateVersion; }
-    public boolean unsignedUpdateAvailable() { return _unsignedUpdateAvailable; }
-    public String unsignedUpdateVersion() { return _unsignedUpdateVersion; }
-
-    /**
-     *  Is the news newer than the last time it was hidden?
-     *  @since 0.8.12
-     */
-    public boolean shouldShowNews() {
-        if (_lastUpdated <= 0)
-            return true;
-        String h = _context.getProperty(PROP_LAST_HIDDEN);
-        if (h == null)
-            return true;
-        long last = 0;
-        try {
-            last = Long.parseLong(h);
-        } catch (NumberFormatException nfe) {}
-        return _lastUpdated > last;
-    }
-
-    /**
-     *  Save config with the timestamp of the current news to hide, or 0 to show
-     *  @since 0.8.12
-     */
-    public void showNews(boolean yes) {
-        long stamp = yes ? 0 : _lastUpdated;
-        _context.router().saveConfig(PROP_LAST_HIDDEN, Long.toString(stamp));
-    }
-
-    /**
-     *  @return HTML
-     */
-    public String status() {
-         StringBuilder buf = new StringBuilder(128);
-         long now = _context.clock().now();
-         buf.append("<i>");
-         if (_lastUpdated > 0) {
-             buf.append(Messages.getString("News last updated {0} ago.",
-                                           DataHelper.formatDuration2(now - _lastUpdated),
-                                           _context))
-                .append('\n');
-         }
-         if (_lastFetch > _lastUpdated) {
-             buf.append(Messages.getString("News last checked {0} ago.",
-                                           DataHelper.formatDuration2(now - _lastFetch),
-                                           _context));
-         }
-         buf.append("</i>");
-         String consoleNonce = System.getProperty("router.consoleNonce");
-         if (_lastUpdated > 0 && consoleNonce != null) {
-             if (shouldShowNews()) {
-                 buf.append(" <a href=\"/?news=0&amp;consoleNonce=").append(consoleNonce).append("\">")
-                    .append(Messages.getString("Hide news", _context));
-             } else {
-                 buf.append(" <a href=\"/?news=1&amp;consoleNonce=").append(consoleNonce).append("\">")
-                    .append(Messages.getString("Show news", _context));
-             }
-             buf.append("</a>");
-         }
-         return buf.toString();
-    }
-    
-    private static final long INITIAL_DELAY = 5*60*1000;
-    private static final long RUN_DELAY = 10*60*1000;
-
-    public void run() {
-        try { Thread.sleep(INITIAL_DELAY + _context.random().nextLong(INITIAL_DELAY)); } catch (InterruptedException ie) {}
-        while (_isRunning) {
-            if (!_updateAvailable) checkForUpdates();
-            if (shouldFetchNews()) {
-                fetchNews();
-                if (shouldFetchUnsigned())
-                    fetchUnsignedHead();
-            }
-            try { Thread.sleep(RUN_DELAY); } catch (InterruptedException ie) {}
-        }
-    }
-    
-    boolean dontInstall() {
-        File test = new File(_context.getBaseDir(), "history.txt");
-        boolean readonly = ((test.exists() && !test.canWrite()) || (!_context.getBaseDir().canWrite()));
-        boolean disabled = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_UPDATE_DISABLED)).booleanValue();
-        return readonly || disabled;
-    }
-
-    private boolean shouldInstall() {
-        String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY);
-        if ("notify".equals(policy) || dontInstall())
-            return false;
-        File zip = new File(_context.getRouterDir(), Router.UPDATE_FILE);
-        return !zip.exists();
-    }
-    
-    private boolean shouldFetchNews() {
-        if (_invalidated)
-            return true;
-        updateLastFetched();
-        String freq = _context.getProperty(ConfigUpdateHandler.PROP_REFRESH_FREQUENCY,
-                                           ConfigUpdateHandler.DEFAULT_REFRESH_FREQUENCY);
-        try {
-            long ms = Long.parseLong(freq);
-            if (ms <= 0)
-                return false;
-            
-            if (_lastFetch + ms < _context.clock().now()) {
-                return true;
-            } else {
-                if (_log.shouldLog(Log.DEBUG))
-                    _log.debug("Last fetched " + DataHelper.formatDuration(_context.clock().now() - _lastFetch) + " ago");
-                return false;
-            }
-        } catch (NumberFormatException nfe) {
-            if (_log.shouldLog(Log.ERROR))
-                _log.error("Invalid refresh frequency: " + freq);
-            return false;
-        }
-    }
-
-    /**
-     *  Call this when changing news URLs to force an update next time the timer fires.
-     *  @since 0.8.7
-     */
-    void invalidateNews() {
-        _lastModified = null;
-        _invalidated = true;
-    }
-
-    public void fetchNews() {
-        String newsURL = ConfigUpdateHelper.getNewsURL(_context);
-        boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
-        String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
-        int proxyPort = ConfigUpdateHandler.proxyPort(_context);
-        if (_tempFile.exists())
-            _tempFile.delete();
-        
-        try {
-            EepGet get = null;
-            if (shouldProxy)
-                get = new EepGet(_context, true, proxyHost, proxyPort, 0, _tempFile.getAbsolutePath(), newsURL, true, null, _lastModified);
-            else
-                get = new EepGet(_context, false, null, 0, 0, _tempFile.getAbsolutePath(), newsURL, true, null, _lastModified);
-            get.addStatusListener(this);
-            if (get.fetch()) {
-                _lastModified = get.getLastModified();
-                _invalidated = false;
-            } else {
-                // backup news location - always proxied
-                _tempFile.delete();
-                get = new EepGet(_context, true, proxyHost, proxyPort, 0, _tempFile.getAbsolutePath(), BACKUP_NEWS_URL, true, null, _lastModified);
-                get.addStatusListener(this);
-                if (get.fetch())
-                    _lastModified = get.getLastModified();
-            }
-        } catch (Throwable t) {
-            _log.error("Error fetching the news", t);
-        }
-    }
-    
-    public boolean shouldFetchUnsigned() {
-        String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
-        return url != null && url.length() > 0 &&
-               Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_UPDATE_UNSIGNED)).booleanValue() &&
-               !dontInstall();
-    }
-
-    /**
-     * HEAD the update url, and if the last-mod time is newer than the last update we
-     * downloaded, as stored in the properties, then we download it using eepget.
-     */
-    public void fetchUnsignedHead() {
-        String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
-        if (url == null || url.length() <= 0)
-            return;
-        // assume always proxied for now
-        //boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
-        String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
-        int proxyPort = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_PORT, ConfigUpdateHandler.DEFAULT_PROXY_PORT_INT);
-
-        try {
-            EepHead get = new EepHead(_context, proxyHost, proxyPort, 0, url);
-            if (get.fetch()) {
-                String lastmod = get.getLastModified();
-                if (lastmod != null) {
-                    long modtime = RFC822Date.parse822Date(lastmod);
-                    if (modtime <= 0) return;
-                    String lastUpdate = _context.getProperty(UpdateHandler.PROP_LAST_UPDATE_TIME);
-                    if (lastUpdate == null) {
-                        // we don't know what version you have, so stamp it with the current time,
-                        // and we'll look for something newer next time around.
-                        _context.router().saveConfig(UpdateHandler.PROP_LAST_UPDATE_TIME,
-                                                           Long.toString(_context.clock().now()));
-                        return;
-                    }
-                    long ms = 0;
-                    try {
-                        ms = Long.parseLong(lastUpdate);
-                    } catch (NumberFormatException nfe) {}
-                    if (ms <= 0) return;
-                    if (modtime > ms) {
-                        _unsignedUpdateAvailable = true;
-                        // '07-Jul 21:09 UTC' with month name in the system locale
-                        _unsignedUpdateVersion = (new SimpleDateFormat("dd-MMM HH:mm")).format(new Date(modtime)) + " UTC";
-                        if (shouldInstall())
-                            fetchUnsigned();
-                    }
-                }
-            }
-        } catch (Throwable t) {
-            _log.error("Error fetching the unsigned update", t);
-        }
-    }
-
-    public void fetchUnsigned() {
-        String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
-        if (url == null || url.length() <= 0)
-            return;
-        UpdateHandler handler = new UnsignedUpdateHandler(_context, url,
-                                                          _unsignedUpdateVersion);
-        handler.update();
-    }
-
-    private static final String VERSION_STRING = "version=\"" + RouterVersion.VERSION + "\"";
-    private static final String VERSION_PREFIX = "version=\"";
-    private void checkForUpdates() {
-        _updateAvailable = false;
-        if ( (!_newsFile.exists()) || (_newsFile.length() <= 0) ) return;
-        FileInputStream in = null;
-        try {
-            in = new FileInputStream(_newsFile);
-            StringBuilder buf = new StringBuilder(128);
-            while (DataHelper.readLine(in, buf)) {
-                int index = buf.indexOf(VERSION_PREFIX);
-                if (index == -1) {
-                    // skip
-                } else {
-                    int end = buf.indexOf("\"", index + VERSION_PREFIX.length());
-                    if (end > index) {
-                        String ver = buf.substring(index+VERSION_PREFIX.length(), end);
-                        if (_log.shouldLog(Log.DEBUG))
-                            _log.debug("Found version: [" + ver + "]");
-                        if (TrustedUpdate.needsUpdate(RouterVersion.VERSION, ver)) {
-                            if (_log.shouldLog(Log.DEBUG))
-                                _log.debug("Our version is out of date, update!");
-                            _updateVersion = ver;
-                            break;
-                        } else {
-                            if (_log.shouldLog(Log.DEBUG))
-                                _log.debug("Our version is current");
-                            return;
-                        }
-                    }
-                }
-                if (buf.indexOf(VERSION_STRING) != -1) {
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug("Our version found, no need to update: " + buf.toString());
-                    return;
-                } else {
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug("No match in " + buf.toString());
-                }
-                buf.setLength(0);
-            }
-        } catch (IOException ioe) {
-            if (_log.shouldLog(Log.WARN))
-                _log.warn("Error checking the news for an update", ioe);
-            return;
-        } finally {
-            if (in != null) try { in.close(); } catch (IOException ioe) {}
-        }
-        // could not find version="0.5.0.1", so there must be an update ;)
-        
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Our version was NOT found (" + RouterVersion.VERSION + "), update needed");
-        _updateAvailable = !dontInstall();
-        
-        if (shouldInstall()) {
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Policy requests update, so we update");
-            UpdateHandler handler = new UpdateHandler(_context);
-            handler.update();
-        } else {
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Policy requests manual update, so we do nothing");
-        }
-    }
-    
-    public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) {
-        // ignore
-    }
-    public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
-        // ignore
-    }
-    public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
-        if (_log.shouldLog(Log.INFO))
-            _log.info("News fetched from " + url + " with " + (alreadyTransferred+bytesTransferred));
-        
-        long now = _context.clock().now();
-        if (_tempFile.exists()) {
-            boolean copied = FileUtil.copy(_tempFile, _newsFile, true, false);
-            if (copied) {
-                _lastUpdated = now;
-                _tempFile.delete();
-                checkForUpdates();
-            } else {
-                if (_log.shouldLog(Log.ERROR))
-                    _log.error("Failed to copy the news file!");
-            }
-        } else {
-            if (_log.shouldLog(Log.WARN))
-                _log.warn("Transfer complete, but no file? - probably 304 Not Modified");
-        }
-        _lastFetch = now;
-        _context.router().saveConfig(PROP_LAST_CHECKED, Long.toString(now));
-    }
-    
-    public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
-        if (_log.shouldLog(Log.WARN))
-            _log.warn("Failed to fetch the news from " + url);
-        _tempFile.delete();
-    }
-    public void headerReceived(String url, int attemptNum, String key, String val) {}
-    public void attempting(String url) {}
-}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NewsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NewsHelper.java
index 709c4505ff..fba5719ba2 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NewsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NewsHelper.java
@@ -2,6 +2,11 @@ package net.i2p.router.web;
 
 import java.io.File;
 
+import net.i2p.data.DataHelper;
+import net.i2p.router.RouterContext;
+import net.i2p.router.update.ConsoleUpdateManager;
+import static net.i2p.update.UpdateType.*;
+
 /**
  *  If news file does not exist, use file from the initialNews directory
  *  in $I2P
@@ -10,6 +15,86 @@ import java.io.File;
  */
 public class NewsHelper extends ContentHelper {
     
+    public static final String PROP_LAST_UPDATE_TIME = "router.updateLastDownloaded";
+    /** @since 0.8.12 */
+    private static final String PROP_LAST_HIDDEN = "routerconsole.newsLastHidden";
+    /** @since 0.9.2 */
+    public static final String PROP_LAST_CHECKED = "routerconsole.newsLastChecked";
+    /** @since 0.9.2 */
+    public static final String PROP_LAST_UPDATED = "routerconsole.newsLastUpdated";
+    public static final String NEWS_FILE = "docs/news.xml";
+
+    /**
+     *  If ANY update is in progress.
+     *  @since 0.9.2 was stored in system properties
+     */
+    public static boolean isAnyUpdateInProgress() {
+        ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
+        if (mgr == null) return false;
+        return mgr.isUpdateInProgress();
+    }
+
+    /**
+     *  If a signed or unsigned router update is in progress.
+     *  Does NOT cover plugins, news, etc.
+     *  @since 0.9.2 was stored in system properties
+     */
+    public static boolean isUpdateInProgress() {
+        ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
+        if (mgr == null) return false;
+        return mgr.isUpdateInProgress(ROUTER_SIGNED) ||
+               mgr.isUpdateInProgress(ROUTER_UNSIGNED) ||
+               mgr.isUpdateInProgress(TYPE_DUMMY);
+    }
+
+    /**
+     *  @since 0.9.2 moved from NewsFetcher
+     */
+    public static boolean isUpdateAvailable() {
+        ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
+        if (mgr == null) return false;
+        return mgr.getUpdateAvailable(ROUTER_SIGNED) != null;
+    }
+
+    /**
+     *  @return null if none
+     *  @since 0.9.2 moved from NewsFetcher
+     */
+    public static String updateVersion() {
+        ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
+        if (mgr == null) return null;
+        return mgr.getUpdateAvailable(ROUTER_SIGNED);
+    }
+
+    /**
+     *  @since 0.9.2 moved from NewsFetcher
+     */
+    public static boolean isUnsignedUpdateAvailable() {
+        ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
+        if (mgr == null) return false;
+        return mgr.getUpdateAvailable(ROUTER_UNSIGNED) != null;
+    }
+
+    /**
+     *  @return null if none
+     *  @since 0.9.2 moved from NewsFetcher
+     */
+    public static String unsignedUpdateVersion() {
+        ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
+        if (mgr == null) return null;
+        return mgr.getUpdateAvailable(ROUTER_UNSIGNED);
+    }
+
+    /**
+     *  @return "" if none
+     *  @since 0.9.2 moved from UpdateHelper
+     */
+    public static String getUpdateStatus() {
+        ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
+        if (mgr == null) return "";
+        return mgr.getStatus();
+    }
+
     @Override
     public String getContent() {
         File news = new File(_page);
@@ -18,8 +103,131 @@ public class NewsHelper extends ContentHelper {
         return super.getContent();
     } 
 
-    /** @since 0.8.12 */
+    /**
+     *  Is the news newer than the last time it was hidden?
+     *  @since 0.8.12
+     */
     public boolean shouldShowNews() {
-        return NewsFetcher.getInstance(_context).shouldShowNews();
+        return shouldShowNews(_context);
+    }
+
+    /**
+     *  @since 0.9.2
+     */
+    public static boolean shouldShowNews(RouterContext ctx) {
+         long lastUpdated = lastUpdated(ctx);
+        if (lastUpdated <= 0)
+            return true;
+        String h = ctx.getProperty(PROP_LAST_HIDDEN);
+        if (h == null)
+            return true;
+        long last = 0;
+        try {
+            last = Long.parseLong(h);
+        } catch (NumberFormatException nfe) {}
+        return lastUpdated > last;
+    }
+
+    /**
+     *  Save config with the timestamp of the current news to hide, or 0 to show
+     *  @since 0.8.12
+     */
+    public void showNews(boolean yes) {
+        showNews(_context, yes);
+    }
+
+    /**
+     *  Save config with the timestamp of the current news to hide, or 0 to show
+     *  @since 0.9.2
+     */
+    public static void showNews(RouterContext ctx, boolean yes) {
+         long lastUpdated = 0;
+/////// FIME from props, or from last mod time?
+        long stamp = yes ? 0 : lastUpdated;
+        ctx.router().saveConfig(PROP_LAST_HIDDEN, Long.toString(stamp));
+    }
+
+    /**
+     *  @return HTML
+     *  @since 0.9.2 moved from NewsFetcher
+     */
+    public String status() {
+        return status(_context);
+    }
+
+    /**
+     *  @return HTML
+     *  @since 0.9.2 moved from NewsFetcher
+     */
+    public static String status(RouterContext ctx) {
+         StringBuilder buf = new StringBuilder(128);
+         long now = ctx.clock().now();
+         buf.append("<i>");
+         long lastUpdated = lastUpdated(ctx);
+         long lastFetch = lastChecked(ctx);
+         if (lastUpdated > 0) {
+             buf.append(Messages.getString("News last updated {0} ago.",
+                                           DataHelper.formatDuration2(now - lastUpdated),
+                                           ctx))
+                .append('\n');
+         }
+         if (lastFetch > lastUpdated) {
+             buf.append(Messages.getString("News last checked {0} ago.",
+                                           DataHelper.formatDuration2(now - lastFetch),
+                                           ctx));
+         }
+         buf.append("</i>");
+         String consoleNonce = System.getProperty("router.consoleNonce");
+         if (lastUpdated > 0 && consoleNonce != null) {
+             if (shouldShowNews(ctx)) {
+                 buf.append(" <a href=\"/?news=0&amp;consoleNonce=").append(consoleNonce).append("\">")
+                    .append(Messages.getString("Hide news", ctx));
+             } else {
+                 buf.append(" <a href=\"/?news=1&amp;consoleNonce=").append(consoleNonce).append("\">")
+                    .append(Messages.getString("Show news", ctx));
+             }
+             buf.append("</a>");
+         }
+         return buf.toString();
+    }
+    
+    /**
+     *  @since 0.9.2 moved from NewsFetcher
+     */
+    public static boolean dontInstall(RouterContext ctx) {
+        File test = new File(ctx.getBaseDir(), "history.txt");
+        boolean readonly = ((test.exists() && !test.canWrite()) || (!ctx.getBaseDir().canWrite()));
+        boolean disabled = ctx.getBooleanProperty(ConfigUpdateHandler.PROP_UPDATE_DISABLED);
+        return readonly || disabled;
+    }
+
+    /**
+     *  @since 0.9.2
+     */
+    public static long lastChecked(RouterContext ctx) {
+        String lc = ctx.getProperty(PROP_LAST_CHECKED);
+        if (lc == null) {
+            try {
+                return Long.parseLong(lc);
+            } catch (NumberFormatException nfe) {}
+        }
+        return 0;
+    }
+
+    /**
+     *  When the news was last downloaded
+     *  @since 0.9.2
+     */
+    public static long lastUpdated(RouterContext ctx) {
+        String lc = ctx.getProperty(PROP_LAST_UPDATED);
+        if (lc == null) {
+            try {
+                return Long.parseLong(lc);
+            } catch (NumberFormatException nfe) {}
+        }
+        File newsFile = new File(ctx.getRouterDir(), NEWS_FILE);
+        long rv = newsFile.lastModified();
+        ctx.router().saveConfig(PROP_LAST_UPDATED, Long.toString(rv));
+        return rv;
     }
 }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
index 06f2e019ca..d5f3f23269 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
@@ -25,6 +25,8 @@ import net.i2p.router.RouterContext;
 import net.i2p.router.RouterVersion;
 import net.i2p.router.startup.ClientAppConfig;
 import net.i2p.router.startup.LoadClientAppsJob;
+import net.i2p.router.update.ConsoleUpdateManager;
+import static net.i2p.update.UpdateType.*;
 import net.i2p.util.ConcurrentHashSet;
 import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
@@ -45,8 +47,9 @@ import org.mortbay.jetty.handler.ContextHandlerCollection;
  */
 public class PluginStarter implements Runnable {
     protected RouterContext _context;
-    static final String PREFIX = "plugin.";
-    static final String ENABLED = ".startOnLoad";
+    public static final String PREFIX = "plugin.";
+    public static final String ENABLED = ".startOnLoad";
+    public static final String PLUGIN_DIR = "plugins";
     private static final String[] STANDARD_WEBAPPS = { "i2psnark", "i2ptunnel", "susidns",
                                                        "susimail", "addressbook", "routerconsole" };
     private static final String[] STANDARD_THEMES = { "images", "light", "dark", "classic",
@@ -66,7 +69,7 @@ public class PluginStarter implements Runnable {
 
     public void run() {
         if (_context.getBooleanPropertyDefaultTrue("plugins.autoUpdate") &&
-            (!Boolean.valueOf(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS)).booleanValue()) &&
+            (!NewsHelper.isUpdateInProgress()) &&
             (!RouterVersion.VERSION.equals(_context.getProperty("router.previousVersion"))))
             updateAll(_context, true);
         startPlugins(_context);
@@ -112,18 +115,24 @@ public class PluginStarter implements Runnable {
         }
         if (toUpdate.isEmpty())
             return;
-        PluginUpdateChecker puc = PluginUpdateChecker.getInstance(ctx);
-        if (puc.isRunning())
+
+        ConsoleUpdateManager mgr = (ConsoleUpdateManager) ctx.updateManager();
+        if (mgr == null)
+            return;
+        if (mgr.isUpdateInProgress())
             return;
 
         if (delay) {
             // wait for proxy
-            System.setProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS, "true");
-            puc.setAppStatus(Messages.getString("Checking for plugin updates", ctx));
-            try {
-                Thread.sleep(3*60*1000);
-            } catch (InterruptedException ie) {}
-            System.setProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS, "false");
+            mgr.update(TYPE_DUMMY, 3*60*1000);
+            mgr.notifyProgress(null, Messages.getString("Checking for plugin updates", ctx));
+            int loop = 0;
+            do {
+                try {
+                    Thread.sleep(5*1000);
+                } catch (InterruptedException ie) {}
+                if (loop++ > 40) break;
+            } while (mgr.isUpdateInProgress(TYPE_DUMMY));
         }
 
         Log log = ctx.logManager().getLog(PluginStarter.class);
@@ -132,34 +141,32 @@ public class PluginStarter implements Runnable {
             String appName = entry.getKey();
             if (log.shouldLog(Log.WARN))
                 log.warn("Checking for update plugin: " + appName);
-            puc.update(appName);
-            do {
-                try {
-                    Thread.sleep(5*1000);
-                } catch (InterruptedException ie) {}
-            } while (puc.isRunning());
-            if (!puc.isNewerAvailable()) {
+
+            // blocking
+            if (mgr.checkAvailable(PLUGIN, appName, 60*1000) == null) {
                 if (log.shouldLog(Log.WARN))
                     log.warn("No update available for plugin: " + appName);
                 continue;
             }
-            PluginUpdateHandler puh = PluginUpdateHandler.getInstance(ctx);
-            String url = entry.getValue();
+
             if (log.shouldLog(Log.WARN))
                 log.warn("Updating plugin: " + appName);
-            puh.update(url);
+            mgr.update(PLUGIN, appName, 30*60*1000);
+            int loop = 0;
             do {
                 try {
                     Thread.sleep(5*1000);
                 } catch (InterruptedException ie) {}
-            } while (puh.isRunning());
-            if (puh.wasUpdateSuccessful())
+                if (loop++ > 40) break;
+            } while (mgr.isUpdateInProgress(PLUGIN, appName));
+
+            if (mgr.getUpdateAvailable(PLUGIN, appName) != null)
                 updated++;
         }
         if (updated > 0)
-            puc.setDoneStatus(ngettext("1 plugin updated", "{0} plugins updated", updated, ctx));
+            mgr.notifyComplete(null, ngettext("1 plugin updated", "{0} plugins updated", updated, ctx));
         else
-            puc.setDoneStatus(Messages.getString("Plugin update check complete", ctx));
+            mgr.notifyComplete(null, Messages.getString("Plugin update check complete", ctx));
     }
 
     /** this shouldn't throw anything */
@@ -189,9 +196,9 @@ public class PluginStarter implements Runnable {
      *  @return true on success
      *  @throws just about anything, caller would be wise to catch Throwable
      */
-    static boolean startPlugin(RouterContext ctx, String appName) throws Exception {
+    public static boolean startPlugin(RouterContext ctx, String appName) throws Exception {
         Log log = ctx.logManager().getLog(PluginStarter.class);
-        File pluginDir = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName);
+        File pluginDir = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName);
         if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) {
             log.error("Cannot start nonexistent plugin: " + appName);
             disablePlugin(appName);
@@ -199,7 +206,7 @@ public class PluginStarter implements Runnable {
         }
 
         // Do we need to extract an update?
-        File pluginUpdate = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName + "/app.xpi2p.zip" );
+        File pluginUpdate = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName + "/app.xpi2p.zip" );
         if(pluginUpdate.exists()) {
             // Compare the start time of the router with the plugin.
             if(ctx.router().getWhenStarted() > pluginUpdate.lastModified()) {
@@ -363,9 +370,9 @@ public class PluginStarter implements Runnable {
      *  @return true on success
      *  @throws just about anything, caller would be wise to catch Throwable
      */
-    static boolean stopPlugin(RouterContext ctx, String appName) throws Exception {
+    public static boolean stopPlugin(RouterContext ctx, String appName) throws Exception {
         Log log = ctx.logManager().getLog(PluginStarter.class);
-        File pluginDir = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName);
+        File pluginDir = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName);
         if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) {
             log.error("Cannot stop nonexistent plugin: " + appName);
             return false;
@@ -424,7 +431,7 @@ public class PluginStarter implements Runnable {
     /** @return true on success - caller should call stopPlugin() first */
     static boolean deletePlugin(RouterContext ctx, String appName) throws Exception {
         Log log = ctx.logManager().getLog(PluginStarter.class);
-        File pluginDir = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName);
+        File pluginDir = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName);
         if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) {
             log.error("Cannot delete nonexistent plugin: " + appName);
             return false;
@@ -469,7 +476,7 @@ public class PluginStarter implements Runnable {
 
     /** plugin.config */
     public static Properties pluginProperties(I2PAppContext ctx, String appName) {
-        File cfgFile = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName + '/' + "plugin.config");
+        File cfgFile = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName + '/' + "plugin.config");
         Properties rv = new Properties();
         try {
             DataHelper.loadProps(rv, cfgFile);
@@ -530,7 +537,7 @@ public class PluginStarter implements Runnable {
      */
     public static List<String> getPlugins() {
         List<String> rv = new ArrayList();
-        File pluginDir = new File(I2PAppContext.getGlobalContext().getConfigDir(), PluginUpdateHandler.PLUGIN_DIR);
+        File pluginDir = new File(I2PAppContext.getGlobalContext().getConfigDir(), PLUGIN_DIR);
         File[] files = pluginDir.listFiles();
         if (files == null)
             return rv;
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java
deleted file mode 100644
index 98ad9f6034..0000000000
--- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java
+++ /dev/null
@@ -1,200 +0,0 @@
-package net.i2p.router.web;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.util.List;
-import java.util.Properties;
-
-import net.i2p.crypto.TrustedUpdate;
-import net.i2p.router.RouterContext;
-import net.i2p.util.EepGet;
-import net.i2p.util.I2PAppThread;
-import net.i2p.util.PartialEepGet;
-import net.i2p.util.SimpleScheduler;
-import net.i2p.util.SimpleTimer;
-import net.i2p.util.VersionComparator;
-
-/**
- * Check for an updated version of a plugin.
- * A plugin is a standard .sud file with a 40-byte signature,
- * a 16-byte version, and a .zip file.
- *
- * So we get the current version and update URL for the installed plugin,
- * then fetch the first 56 bytes of the URL, extract the version,
- * and compare.
- *
- * @since 0.7.12
- * @author zzz
- */
-public class PluginUpdateChecker extends UpdateHandler {
-    private static PluginUpdateCheckerRunner _pluginUpdateCheckerRunner;
-    private String _appName;
-    private String _oldVersion;
-    private String _xpi2pURL;
-    private volatile boolean _isNewerAvailable;
-
-    private static PluginUpdateChecker _instance;
-    public static final synchronized PluginUpdateChecker getInstance(RouterContext ctx) { 
-        if (_instance != null)
-            return _instance;
-        _instance = new PluginUpdateChecker(ctx);
-        return _instance;
-    }
-
-    private PluginUpdateChecker(RouterContext ctx) {
-        super(ctx);
-    }
-    
-    /**
-     *  check all plugins
-     *  @deprecated not finished
-     */
-    public void update() {
-        Thread t = new I2PAppThread(new AllCheckerRunner(), "AllAppChecker", true);
-        t.start();
-    }
-
-    /**
-     *  check all plugins
-     *  @deprecated not finished
-     */
-    public class AllCheckerRunner implements Runnable {
-        public void run() {
-            List<String> plugins = PluginStarter.getPlugins();
-            // TODO
-        }
-    }
-
-    /** check a single plugin */
-    public void update(String appName) {
-        // don't block waiting for the other one to finish
-        if ("true".equals(System.getProperty(PROP_UPDATE_IN_PROGRESS))) {
-            _log.error("Update already running");
-            return;
-        }
-        synchronized (UpdateHandler.class) {
-            Properties props = PluginStarter.pluginProperties(_context, appName);
-            String oldVersion = props.getProperty("version");
-            String xpi2pURL = props.getProperty("updateURL");
-            if (oldVersion == null || xpi2pURL == null) {
-                updateStatus("<b>" + _("Cannot check, plugin {0} is not installed", appName) + "</b>");
-                return;
-            }
-
-            if (_pluginUpdateCheckerRunner == null)
-                _pluginUpdateCheckerRunner = new PluginUpdateCheckerRunner();
-            if (_pluginUpdateCheckerRunner.isRunning())
-                return;
-            _xpi2pURL = xpi2pURL;
-            _appName = appName;
-            _oldVersion = oldVersion;
-            _isNewerAvailable = false;
-            System.setProperty(PROP_UPDATE_IN_PROGRESS, "true");
-            I2PAppThread update = new I2PAppThread(_pluginUpdateCheckerRunner, "AppChecker", true);
-            update.start();
-        }
-    }
-    
-    /** @since 0.8.13 */
-    public void setAppStatus(String status) {
-        updateStatus(status);
-    }
-    
-    /** @since 0.8.13 */
-    public void setDoneStatus(String status) {
-        updateStatus(status);
-        scheduleStatusClean(status);
-    }
-
-    public boolean isRunning() {
-        return _pluginUpdateCheckerRunner != null && _pluginUpdateCheckerRunner.isRunning();
-    }
-    
-    @Override
-    public boolean isDone() {
-        // FIXME
-        return false;
-    }
-    
-    /** @since 0.8.13 */
-    public boolean isNewerAvailable() {
-        return _isNewerAvailable;
-    }
-
-    private void scheduleStatusClean(String msg) {
-        SimpleScheduler.getInstance().addEvent(new Cleaner(msg), 20*60*1000);
-    }
-
-    private class Cleaner implements SimpleTimer.TimedEvent {
-        private final String _msg;
-        public Cleaner(String msg) {
-            _msg = msg;
-        }
-        public void timeReached() {
-            if (_msg.equals(getStatus()))
-                updateStatus("");
-        }
-    }
-
-    public class PluginUpdateCheckerRunner extends UpdateRunner implements Runnable, EepGet.StatusListener {
-        ByteArrayOutputStream _baos;
-
-        public PluginUpdateCheckerRunner() { 
-            super();
-            _baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES);
-        }
-
-        @Override
-        protected void update() {
-            _isNewerAvailable = false;
-            updateStatus("<b>" + _("Checking for update of plugin {0}", _appName) + "</b>");
-            // use the same settings as for updater
-            // always proxy, or else FIXME
-            //boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
-            String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
-            int proxyPort = ConfigUpdateHandler.proxyPort(_context);
-            _baos.reset();
-            try {
-                _get = new PartialEepGet(_context, proxyHost, proxyPort, _baos, _xpi2pURL, TrustedUpdate.HEADER_BYTES);
-                _get.addStatusListener(PluginUpdateCheckerRunner.this);
-                _get.fetch();
-            } catch (Throwable t) {
-                _log.error("Error checking update for plugin", t);
-            }
-        }
-        
-        public boolean isNewerAvailable() {
-            return _isNewerAvailable;
-        }
-
-        @Override
-        public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
-        }
-
-        @Override
-        public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
-            String newVersion = TrustedUpdate.getVersionString(new ByteArrayInputStream(_baos.toByteArray()));
-            boolean newer = (new VersionComparator()).compare(newVersion, _oldVersion) > 0;
-            String msg;
-            if (newer) {
-                msg = "<b>" + _("New plugin version {0} is available", newVersion) + "</b>";
-                _isNewerAvailable = true;
-            } else {
-                msg = "<b>" + _("No new version is available for plugin {0}", _appName) + "</b>";
-            }
-            updateStatus(msg);
-            scheduleStatusClean(msg);
-        }
-
-        @Override
-        public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
-            File f = new File(_updateFile);
-            f.delete();
-            String msg = "<b>" + _("Update check failed for plugin {0}", _appName) + "</b>";
-            updateStatus(msg);
-            scheduleStatusClean(msg);
-        }
-    }
-}
-    
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
index 0f54e4baf6..332fcec297 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
@@ -27,6 +27,7 @@ import net.i2p.apps.systray.SysTray;
 import net.i2p.data.Base32;
 import net.i2p.data.DataHelper;
 import net.i2p.router.RouterContext;
+import net.i2p.router.update.ConsoleUpdateManager;
 import net.i2p.util.Addresses;
 import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
@@ -538,10 +539,8 @@ public class RouterConsoleRunner {
         if (contexts != null) {
             RouterContext ctx = contexts.get(0);
 
-            NewsFetcher fetcher = NewsFetcher.getInstance(ctx);
-            Thread newsThread = new I2PAppThread(fetcher, "NewsFetcher", true);
-            newsThread.setPriority(Thread.NORM_PRIORITY - 1);
-            newsThread.start();
+            ConsoleUpdateManager um = new ConsoleUpdateManager(ctx);
+            um.start();
         
             if (PluginStarter.pluginsEnabled(ctx)) {
                 t = new I2PAppThread(new PluginStarter(ctx), "PluginStarter", true);
@@ -549,7 +548,6 @@ public class RouterConsoleRunner {
                 t.start();
                 ctx.addShutdownTask(new PluginStopper(ctx));
             }
-            ctx.addShutdownTask(new NewsShutdown(fetcher, newsThread));
             // stat summarizer registers its own hook
             ctx.addShutdownTask(new ServerShutdown());
             ConfigServiceHandler.registerSignalHandler(ctx);
@@ -721,22 +719,6 @@ public class RouterConsoleRunner {
         }
     }
     
-    /** @since 0.8.8 */
-    private static class NewsShutdown implements Runnable {
-        private final NewsFetcher _fetcher;
-        private final Thread _newsThread;
-
-        public NewsShutdown(NewsFetcher fetcher, Thread t) {
-            _fetcher = fetcher;
-            _newsThread = t;
-        }
-
-        public void run() {
-            _fetcher.shutdown();
-            _newsThread.interrupt();
-        }
-    }
-
     public static Properties webAppProperties() {
         return webAppProperties(I2PAppContext.getGlobalContext().getConfigDir().getAbsolutePath());
     }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
index 4e7d2f23fb..e799147434 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
@@ -594,19 +594,19 @@ public class SummaryHelper extends HelperBase {
 ********/
 
     public boolean updateAvailable() { 
-        return NewsFetcher.getInstance(_context).updateAvailable();
+        return NewsHelper.isUpdateAvailable();
     }
 
     public boolean unsignedUpdateAvailable() { 
-        return NewsFetcher.getInstance(_context).unsignedUpdateAvailable();
+        return NewsHelper.isUnsignedUpdateAvailable();
     }
 
     public String getUpdateVersion() { 
-        return NewsFetcher.getInstance(_context).updateVersion();
+        return NewsHelper.updateVersion();
     }
 
     public String getUnsignedUpdateVersion() { 
-        return NewsFetcher.getInstance(_context).unsignedUpdateVersion();
+        return NewsHelper.unsignedUpdateVersion();
     }
 
     /**
@@ -616,12 +616,12 @@ public class SummaryHelper extends HelperBase {
     public String getUpdateStatus() {
         StringBuilder buf = new StringBuilder(512);
         // display all the time so we display the final failure message, and plugin update messages too
-        String status = UpdateHandler.getStatus();
+        String status = NewsHelper.getUpdateStatus();
         if (status.length() > 0) {
             buf.append("<h4>").append(status).append("</h4><hr>\n");
         }
         if (updateAvailable() || unsignedUpdateAvailable()) {
-            if ("true".equals(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS))) {
+            if (NewsHelper.isUpdateInProgress()) {
                 // nothing
             } else if(
                       // isDone() is always false for now, see UpdateHandler
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/UnsignedUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/UnsignedUpdateHandler.java
deleted file mode 100644
index f728783758..0000000000
--- a/apps/routerconsole/java/src/net/i2p/router/web/UnsignedUpdateHandler.java
+++ /dev/null
@@ -1,130 +0,0 @@
-package net.i2p.router.web;
-
-import java.io.File;
-
-import net.i2p.router.Router;
-import net.i2p.router.RouterContext;
-import net.i2p.router.util.RFC822Date;
-import net.i2p.util.EepGet;
-import net.i2p.util.FileUtil;
-import net.i2p.util.I2PAppThread;
-import net.i2p.util.Log;
-
-/**
- * <p>Handles the request to update the router by firing off an
- * {@link net.i2p.util.EepGet} call to download the latest unsigned zip file
- * and displaying the status to anyone who asks.
- * </p>
- * <p>After the download completes the signed update file is copied to the
- * router directory, and if configured the router is restarted to complete
- * the update process.
- * </p>
- */
-public class UnsignedUpdateHandler extends UpdateHandler {
-    private static UnsignedUpdateRunner _unsignedUpdateRunner;
-    private String _zipURL;
-    private String _zipVersion;
-
-    public UnsignedUpdateHandler(RouterContext ctx, String zipURL, String version) {
-        super(ctx);
-        _zipURL = zipURL;
-        _zipVersion = version;
-        _updateFile = (new File(ctx.getTempDir(), "tmp" + ctx.random().nextInt() + Router.UPDATE_FILE)).getAbsolutePath();
-    }
-    
-    @Override
-    public void update() {
-        // don't block waiting for the other one to finish
-        if ("true".equals(System.getProperty(PROP_UPDATE_IN_PROGRESS))) {
-            _log.error("Update already running");
-            return;
-        }
-        synchronized (UpdateHandler.class) {
-            if (_unsignedUpdateRunner == null) {
-                _unsignedUpdateRunner = new UnsignedUpdateRunner();
-            }
-            if (_unsignedUpdateRunner.isRunning()) {
-                return;
-            } else {
-                System.setProperty(PROP_UPDATE_IN_PROGRESS, "true");
-                I2PAppThread update = new I2PAppThread(_unsignedUpdateRunner, "UnsignedUpdate");
-                update.start();
-            }
-        }
-    }
-    
-    /**
-     *  Eepget the .zip file to the temp dir, then copy it over
-     */
-    public class UnsignedUpdateRunner extends UpdateRunner implements Runnable, EepGet.StatusListener {
-        public UnsignedUpdateRunner() { 
-            super();
-        }
-
-        /** Get the file */
-        @Override
-        protected void update() {
-            updateStatus("<b>" + _("Updating") + "</b>");
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Starting unsigned update URL: " + _zipURL);
-            // always proxy for now
-            //boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
-            String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
-            int proxyPort = ConfigUpdateHandler.proxyPort(_context);
-            try {
-                // 40 retries!!
-                _get = new EepGet(_context, proxyHost, proxyPort, 40, _updateFile, _zipURL, false);
-                _get.addStatusListener(UnsignedUpdateRunner.this);
-                _get.fetch();
-            } catch (Throwable t) {
-                _log.error("Error updating", t);
-            }
-        }
-        
-        /** eepget listener callback Overrides */
-        @Override
-        public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
-            File updFile = new File(_updateFile);
-            if (FileUtil.verifyZip(updFile)) {
-                updateStatus("<b>" + _("Update downloaded") + "</b>");
-            } else {
-                updFile.delete();
-                updateStatus("<b>" + _("Unsigned update file from {0} is corrupt", url) + "</b>");
-                _log.log(Log.CRIT, "Corrupt zip file from " + url);
-                return;
-            }
-            File to = new File(_context.getRouterDir(), Router.UPDATE_FILE);
-            boolean copied = FileUtil.copy(updFile, to, true, false);
-            if (copied) {
-                updFile.delete();
-                String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY);
-                this.done = true;
-                String lastmod = _get.getLastModified();
-                long modtime = 0;
-                if (lastmod != null)
-                    modtime = RFC822Date.parse822Date(lastmod);
-                if (modtime <= 0)
-                    modtime = _context.clock().now();
-                _context.router().saveConfig(PROP_LAST_UPDATE_TIME, "" + modtime);
-                if ("install".equals(policy)) {
-                    _log.log(Log.CRIT, "Update was downloaded, restarting to install it");
-                    updateStatus("<b>" + _("Update downloaded") + "</b><br>" + _("Restarting"));
-                    restart();
-                } else {
-                    _log.log(Log.CRIT, "Update was downloaded, will be installed at next restart");
-                    StringBuilder buf = new StringBuilder(64);
-                    buf.append("<b>").append(_("Update downloaded")).append("</b><br>");
-                    if (_context.hasWrapper())
-                        buf.append(_("Click Restart to install"));
-                    else
-                        buf.append(_("Click Shutdown and restart to install"));
-                        buf.append(' ').append(_("Version {0}", _zipVersion));
-                    updateStatus(buf.toString());
-                }
-            } else {
-                _log.log(Log.CRIT, "Failed copy to " + to);
-                updateStatus("<b>" + _("Failed copy to {0}", to.getAbsolutePath()) + "</b>");
-            }
-        }
-    }
-}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java
index 6ebb2a469d..412a907283 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java
@@ -1,25 +1,10 @@
 package net.i2p.router.web;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.text.DecimalFormat;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.StringTokenizer;
-
-import net.i2p.crypto.TrustedUpdate;
-import net.i2p.data.DataHelper;
-import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
-import net.i2p.router.RouterVersion;
-import net.i2p.router.util.RFC822Date;
-import net.i2p.util.EepGet;
-import net.i2p.util.I2PAppThread;
+import net.i2p.router.update.ConsoleUpdateManager;
+import net.i2p.update.UpdateType;
+import static net.i2p.update.UpdateType.*;
 import net.i2p.util.Log;
-import net.i2p.util.PartialEepGet;
-import net.i2p.util.VersionComparator;
 
 /**
  * <p>Handles the request to update the router by firing one or more
@@ -31,27 +16,22 @@ import net.i2p.util.VersionComparator;
  * of the signed update file is unpacked and the router is restarted to complete
  * the update process.
  * </p>
+ *
+ * This is like a FormHandler but we don't extend it, as we don't have the message area, etc.
  */
 public class UpdateHandler {
-    protected static UpdateRunner _updateRunner;
     protected RouterContext _context;
     protected Log _log;
-    protected String _updateFile;
-    private static String _status = "";
     private String _action;
     private String _nonce;
     
-    protected static final String SIGNED_UPDATE_FILE = "i2pupdate.sud";
-    static final String PROP_UPDATE_IN_PROGRESS = "net.i2p.router.web.UpdateHandler.updateInProgress";
-    protected static final String PROP_LAST_UPDATE_TIME = "router.updateLastDownloaded";
-
     public UpdateHandler() {
         this(ContextHelper.getContext(null));
     }
+
     public UpdateHandler(RouterContext ctx) {
         _context = ctx;
         _log = ctx.logManager().getLog(UpdateHandler.class);
-        _updateFile = (new File(ctx.getRouterDir(), SIGNED_UPDATE_FILE)).getAbsolutePath();
     }
     
     /**
@@ -85,272 +65,21 @@ public class UpdateHandler {
         if (_nonce.equals(System.getProperty("net.i2p.router.web.UpdateHandler.nonce")) ||
             _nonce.equals(System.getProperty("net.i2p.router.web.UpdateHandler.noncePrev"))) {
             if (_action.contains("Unsigned")) {
-                // Not us, have NewsFetcher instantiate the correct class.
-                NewsFetcher fetcher = NewsFetcher.getInstance(_context);
-                fetcher.fetchUnsigned();
+                update(ROUTER_UNSIGNED);
             } else {
-                update();
+                update(ROUTER_SIGNED);
             }
         }
     }
 
-    public void update() {
-        // don't block waiting for the other one to finish
-        if ("true".equals(System.getProperty(PROP_UPDATE_IN_PROGRESS))) {
+    private void update(UpdateType type) {
+        ConsoleUpdateManager mgr = (ConsoleUpdateManager) _context.updateManager();
+        if (mgr == null)
+            return;
+        if (mgr.isUpdateInProgress(ROUTER_SIGNED) || mgr.isUpdateInProgress(ROUTER_UNSIGNED)) {
             _log.error("Update already running");
             return;
         }
-        synchronized (UpdateHandler.class) {
-            if (_updateRunner == null)
-                _updateRunner = new UpdateRunner();
-            if (_updateRunner.isRunning()) {
-                return;
-            } else {
-                System.setProperty(PROP_UPDATE_IN_PROGRESS, "true");
-                I2PAppThread update = new I2PAppThread(_updateRunner, "SignedUpdate");
-                update.start();
-            }
-        }
-    }
-    
-    public static String getStatus() {
-        return _status;
-    }
-    
-    public boolean isDone() {
-        return false;
-        // this needs to be fixed and tested
-        //if(this._updateRunner == null)
-        //    return true;
-        //return this._updateRunner.isDone();
-    }
-    
-    public class UpdateRunner implements Runnable, EepGet.StatusListener {
-        protected volatile boolean _isRunning;
-        protected boolean done;
-        protected EepGet _get;
-        protected final DecimalFormat _pct = new DecimalFormat("0.0%");
-        /** tells the listeners what mode we are in */
-        private boolean _isPartial;
-        /** set by the listeners on completion */
-        private boolean _isNewer;
-        private ByteArrayOutputStream _baos;
-
-        public UpdateRunner() { 
-            _isRunning = false;
-            this.done = false;
-            updateStatus("<b>" + _("Updating") + "</b>");
-        }
-        public boolean isRunning() { return _isRunning; }
-        public boolean isDone() {
-            return this.done;
-        }
-        public void run() {
-            _isRunning = true;
-            update();
-            System.setProperty(PROP_UPDATE_IN_PROGRESS, "false");
-            _isRunning = false;
-        }
-
-        /**
-         *  Loop through the entire list of update URLs.
-         *  For each one, first get the version from the first 56 bytes and see if
-         *  it is newer than what we are running now.
-         *  If it is, get the whole thing.
-         */
-        protected void update() {
-            // Do a PartialEepGet on the selected URL, check for version we expect,
-            // and loop if it isn't what we want.
-            // This will allows us to do a release without waiting for the last host to install the update.
-            // Alternative: In bytesTransferred(), Check the data in the output file after
-            // we've received at least 56 bytes. Need a cancel() method in EepGet ?
-
-            boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
-            String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
-            int proxyPort = ConfigUpdateHandler.proxyPort(_context);
-
-            List<String> urls = getUpdateURLs();
-            if (urls.isEmpty()) {
-                // not likely, don't bother translating
-                updateStatus("<b>Update source list is empty, cannot download update</b>");
-                _log.log(Log.CRIT, "Update source list is empty - cannot download update");
-                return;
-            }
-
-            if (shouldProxy)
-                _baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES);
-            for (String updateURL : urls) {
-                updateStatus("<b>" + _("Updating from {0}", linkify(updateURL)) + "</b>");
-                if (_log.shouldLog(Log.DEBUG))
-                    _log.debug("Selected update URL: " + updateURL);
-
-                // Check the first 56 bytes for the version
-                if (shouldProxy) {
-                    _isPartial = true;
-                    _isNewer = false;
-                    _baos.reset();
-                    try {
-                        // no retries
-                        _get = new PartialEepGet(_context, proxyHost, proxyPort, _baos, updateURL, TrustedUpdate.HEADER_BYTES);
-                        _get.addStatusListener(UpdateRunner.this);
-                        _get.fetch();
-                    } catch (Throwable t) {
-                        _isNewer = false;
-                    }
-                    _isPartial = false;
-                    if (!_isNewer)
-                        continue;
-                }
-
-                // Now get the whole thing
-                try {
-                    if (shouldProxy)
-                        // 40 retries!!
-                        _get = new EepGet(_context, proxyHost, proxyPort, 40, _updateFile, updateURL, false);
-                    else
-                        _get = new EepGet(_context, 1, _updateFile, updateURL, false);
-                    _get.addStatusListener(UpdateRunner.this);
-                    _get.fetch();
-                } catch (Throwable t) {
-                    _log.error("Error updating", t);
-                }
-                if (this.done)
-                    break;
-            }
-        }
-        
-        // EepGet Listeners below.
-        // We use the same for both the partial and the full EepGet,
-        // with a couple of adjustments depending on which mode.
-
-        public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) {
-            _isNewer = false;
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Attempt failed on " + url, cause);
-            // ignored
-        }
-        public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
-            if (_isPartial)
-                return;
-            StringBuilder buf = new StringBuilder(64);
-            buf.append("<b>").append(_("Updating")).append("</b> ");
-            double pct = ((double)alreadyTransferred + (double)currentWrite) /
-                         ((double)alreadyTransferred + (double)currentWrite + bytesRemaining);
-            synchronized (_pct) {
-                buf.append(_pct.format(pct));
-            }
-            buf.append(":<br>\n");
-            buf.append(_("{0}B transferred", DataHelper.formatSize2(currentWrite + alreadyTransferred)));
-            updateStatus(buf.toString());
-        }
-        public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
-            if (_isPartial) {
-                // Compare version with what we have now
-                String newVersion = TrustedUpdate.getVersionString(new ByteArrayInputStream(_baos.toByteArray()));
-                boolean newer = (new VersionComparator()).compare(newVersion, RouterVersion.VERSION) > 0;
-                if (!newer) {
-                    updateStatus("<b>" + _("No new version found at {0}", linkify(url)) + "</b>");
-                    if (_log.shouldLog(Log.WARN))
-                        _log.warn("Found old version \"" + newVersion + "\" at " + url);
-                }
-                _isNewer = newer;
-                return;
-            }
-            // Process the .sud/.su2 file
-            updateStatus("<b>" + _("Update downloaded") + "</b>");
-            TrustedUpdate up = new TrustedUpdate(_context);
-            File f = new File(_updateFile);
-            File to = new File(_context.getRouterDir(), Router.UPDATE_FILE);
-            String err = up.migrateVerified(RouterVersion.VERSION, f, to);
-            f.delete();
-            if (err == null) {
-                String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY);
-                this.done = true;
-                // So unsigned update handler doesn't overwrite unless newer.
-                String lastmod = _get.getLastModified();
-                long modtime = 0;
-                if (lastmod != null)
-                    modtime = RFC822Date.parse822Date(lastmod);
-                if (modtime <= 0)
-                    modtime = _context.clock().now();
-                _context.router().saveConfig(PROP_LAST_UPDATE_TIME, "" + modtime);
-                if ("install".equals(policy)) {
-                    _log.log(Log.CRIT, "Update was VERIFIED, restarting to install it");
-                    updateStatus("<b>" + _("Update verified") + "</b><br>" + _("Restarting"));
-                    restart();
-                } else {
-                    _log.log(Log.CRIT, "Update was VERIFIED, will be installed at next restart");
-                    StringBuilder buf = new StringBuilder(64);
-                    buf.append("<b>").append(_("Update downloaded")).append("<br>");
-                    if (_context.hasWrapper())
-                        buf.append(_("Click Restart to install"));
-                    else
-                        buf.append(_("Click Shutdown and restart to install"));
-                    if (up.newVersion() != null)
-                        buf.append(' ').append(_("Version {0}", up.newVersion()));
-                    buf.append("</b>");
-                    updateStatus(buf.toString());
-                }
-            } else {
-                _log.log(Log.CRIT, err + " from " + url);
-                updateStatus("<b>" + err + ' ' + _("from {0}", linkify(url)) + " </b>");
-            }
-        }
-        public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
-            _isNewer = false;
-            // don't display bytesTransferred as it is meaningless
-            _log.error("Update from " + url + " did not download completely (" +
-                               bytesRemaining + " remaining after " + currentAttempt + " tries)");
-
-            updateStatus("<b>" + _("Transfer failed from {0}", linkify(url)) + "</b>");
-        }
-        public void headerReceived(String url, int attemptNum, String key, String val) {}
-        public void attempting(String url) {}
-    }
-    
-    protected void restart() {
-        if (_context.hasWrapper())
-            ConfigServiceHandler.registerWrapperNotifier(_context, Router.EXIT_GRACEFUL_RESTART, false);
-        _context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
-    }
-
-    private List<String> getUpdateURLs() {
-        String URLs = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_URL, ConfigUpdateHandler.DEFAULT_UPDATE_URL);
-        StringTokenizer tok = new StringTokenizer(URLs, " ,\r\n");
-        List<String> URLList = new ArrayList();
-        while (tok.hasMoreTokens())
-            URLList.add(tok.nextToken().trim());
-        Collections.shuffle(URLList, _context.random());
-        return URLList;
-    }
-    
-    protected void updateStatus(String s) {
-        _status = s;
-    }
-
-    protected static String linkify(String url) {
-        return "<a target=\"_blank\" href=\"" + url + "\"/>" + url + "</a>";
-    }
-
-    /** translate a string */
-    protected String _(String s) {
-        return Messages.getString(s, _context);
+        mgr.update(type);
     }
-
-    /**
-     *  translate a string with a parameter
-     *  This is a lot more expensive than _(s), so use sparingly.
-     *
-     *  @param s string to be translated containing {0}
-     *    The {0} will be replaced by the parameter.
-     *    Single quotes must be doubled, i.e. ' -> '' in the string.
-     *  @param o parameter, not translated.
-     *    To tranlslate parameter also, use _("foo {0} bar", _("baz"))
-     *    Do not double the single quotes in the parameter.
-     *    Use autoboxing to call with ints, longs, floats, etc.
-     */
-    protected String _(String s, Object o) {
-        return Messages.getString(s, o, _context);
-    }
-
 }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java b/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java
index 98ca931133..5afa5d2db0 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java
@@ -64,7 +64,7 @@ public class WebAppConfiguration implements Configuration {
         File libDir = new File(i2pContext.getBaseDir(), "lib");
         // FIXME this only works if war is the same name as the plugin
         File pluginDir = new File(i2pContext.getConfigDir(),
-                                        PluginUpdateHandler.PLUGIN_DIR + ctxPath);
+                                        PluginStarter.PLUGIN_DIR + ctxPath);
 
         File dir = libDir;
         String cp;
diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java
index 313f6e445e..6a24158ec3 100644
--- a/core/java/src/net/i2p/I2PAppContext.java
+++ b/core/java/src/net/i2p/I2PAppContext.java
@@ -26,6 +26,7 @@ import net.i2p.data.Base64;
 import net.i2p.data.RoutingKeyGenerator;
 import net.i2p.internal.InternalClientManager;
 import net.i2p.stat.StatManager;
+import net.i2p.update.UpdateManager;
 import net.i2p.util.Clock;
 import net.i2p.util.ConcurrentHashSet;
 import net.i2p.util.FileUtil;
@@ -984,4 +985,13 @@ public class I2PAppContext {
             _simpleTimer2Initialized = true;
         }
     }
+
+    /**
+     *  The controller of router, plugin, and other updates.
+     *  @return always null in I2PAppContext, the update manager if in RouterContext and it is registered
+     *  @since 0.9.2
+     */
+    public UpdateManager updateManager() {
+        return null;
+    }
 }
diff --git a/core/java/src/net/i2p/update/UpdateManager.java b/core/java/src/net/i2p/update/UpdateManager.java
new file mode 100644
index 0000000000..4099822bcc
--- /dev/null
+++ b/core/java/src/net/i2p/update/UpdateManager.java
@@ -0,0 +1,79 @@
+package net.i2p.update;
+
+import java.io.File;
+import java.net.URI;
+import java.util.List;;
+
+/**
+ *  The central resource coordinating updates.
+ *  This must be registered with the context.
+ *
+ *  The UpdateManager starts and stops all updates,
+ *  and controls notification to the user.
+ *
+ *  @since 0.9.2
+ */
+public interface UpdateManager {
+    
+    /**
+     *  Call multiple times for each type/method pair.
+     *  The UpdateManager will then call start()
+     */
+    public void register(Updater updater, UpdateType type, UpdateMethod method, int priority);
+
+    public void unregister(Updater updater, UpdateType type, UpdateMethod method);
+    
+    public void start();
+
+    public void shutdown();
+
+    /**
+     *  Called by the Updater, either after check() was called, or it found out on its own.
+     *
+     *  @param newsSource who told us
+     *  @param id plugin name for plugins, ignored otherwise
+     *  @param method How to get the new version
+     *  @param updateSourcew Where to get the new version
+     *  @param newVersion The new version available
+     *  @param minVersion The minimum installed version to be able to update to newVersion
+     *  @return true if we didn't know already
+     */
+    public boolean notifyVersionAvailable(UpdateTask task, URI newsSource,
+                                          UpdateType type, String id,
+                                          UpdateMethod method, List<URI> updateSources,
+                                          String newVersion, String minVersion);
+
+    /**
+     *  Called by the Updater after check() was called and all notifyVersionAvailable() callbacks are finished
+     *  @param newer notifyVersionAvailable was called
+     *  @param success check succeeded (newer or not)
+     */
+    public void notifyCheckComplete(UpdateTask task, boolean newer, boolean success);
+
+    public void notifyProgress(UpdateTask task, String status);
+    public void notifyProgress(UpdateTask task, String status, long downloaded, long totalSize);
+
+    /**
+     *  Not necessarily the end if there are more URIs to try.
+     *  @param t may be null
+     */
+    public void notifyAttemptFailed(UpdateTask task, String reason, Throwable t);
+
+    /**
+     *  The task has finished and failed.
+     *  @param t may be null
+     */
+    public void notifyTaskFailed(UpdateTask task, String reason, Throwable t);
+
+    /**
+     *  An update has been downloaded but not verified.
+     *  The manager will verify it.
+     *  Caller should delete the file upon return, unless it will share it with others,
+     *  e.g. on a torrent.
+     *
+     *  @param actualVersion may be higher (or lower?) than the version requested
+     *  @param file a valid format for the task's UpdateType
+     *  @return true if valid, false if corrupt
+     */
+    public boolean notifyComplete(UpdateTask task, String actualVersion, File file);
+}
diff --git a/core/java/src/net/i2p/update/UpdateMethod.java b/core/java/src/net/i2p/update/UpdateMethod.java
new file mode 100644
index 0000000000..6d30bc99c8
--- /dev/null
+++ b/core/java/src/net/i2p/update/UpdateMethod.java
@@ -0,0 +1,15 @@
+package net.i2p.update;
+
+/**
+ *  Transport mechanism for getting something.
+ *
+ *  @since 0.9.2
+ */
+public enum UpdateMethod {
+    METHOD_DUMMY,
+    HTTP,              // .i2p or via outproxy
+    HTTP_CLEARNET,     // direct non-.i2p
+    TORRENT,
+    GNUTELLA, IMULE, TAHOE_LAFS,
+    DEBIAN
+}
diff --git a/core/java/src/net/i2p/update/UpdateTask.java b/core/java/src/net/i2p/update/UpdateTask.java
new file mode 100644
index 0000000000..b9237b3ee6
--- /dev/null
+++ b/core/java/src/net/i2p/update/UpdateTask.java
@@ -0,0 +1,30 @@
+package net.i2p.update;
+
+import java.net.URI;
+
+/**
+ *  A running check or download. Cannot be restarted.
+ *
+ *  @since 0.9.2
+ */
+public interface UpdateTask {
+    
+    public void shutdown();
+
+    public boolean isRunning();
+
+    public UpdateType getType();
+
+    public UpdateMethod getMethod();
+
+   /**
+    *  The current URI being checked or downloaded from.
+    *  Can change if there are multiple URIs to try.
+    */
+    public URI getURI();
+
+   /**
+    *  Valid for plugins
+    */
+    public String getID();
+}
diff --git a/core/java/src/net/i2p/update/UpdateType.java b/core/java/src/net/i2p/update/UpdateType.java
new file mode 100644
index 0000000000..f16eaad7d2
--- /dev/null
+++ b/core/java/src/net/i2p/update/UpdateType.java
@@ -0,0 +1,16 @@
+package net.i2p.update;
+
+/**
+ *  What to update
+ *
+ *  @since 0.9.2
+ */
+public enum UpdateType {
+    TYPE_DUMMY,
+    NEWS,
+    ROUTER_SIGNED,
+    ROUTER_SIGNED_PACK200,      // unused, use ROUTER_SIGNED for both
+    ROUTER_UNSIGNED,
+    PLUGIN, PLUGIN_INSTALL,
+    GEOIP, BLOCKLIST, RESEED
+}
diff --git a/core/java/src/net/i2p/update/Updater.java b/core/java/src/net/i2p/update/Updater.java
new file mode 100644
index 0000000000..bea1a778db
--- /dev/null
+++ b/core/java/src/net/i2p/update/Updater.java
@@ -0,0 +1,36 @@
+package net.i2p.update;
+
+import java.net.URI;
+import java.util.List;
+
+/**
+ *  Controls one or more types of updates.
+ *  This must be registered with the UpdateManager.
+ *
+ *  @since 0.9.2
+ */
+public interface Updater {
+    
+    /**
+     *  Check for updates.
+     *  Should not block.
+     *  If any are found, call back to UpdateManager.notifyUpdateAvailable().
+     *
+     *  @param id plugin name or ignored
+     *  @param maxTime how long you have
+     *  @return active task or null if unable to check
+     */
+    public UpdateTask check(UpdateType type, UpdateMethod method,
+                            String id, String currentVersion, long maxTime);
+
+    /**
+     *  Start a download and return a handle to the download task.
+     *  Should not block.
+     *
+     *  @param id plugin name or ignored
+     *  @param maxTime how long you have
+     *  @return active task or null if unable to download
+     */
+    public UpdateTask update(UpdateType type, UpdateMethod method, List<URI> updateSources,
+                               String id, String newVersion, long maxTime);
+}
diff --git a/core/java/src/net/i2p/update/package.html b/core/java/src/net/i2p/update/package.html
new file mode 100644
index 0000000000..20e06658ea
--- /dev/null
+++ b/core/java/src/net/i2p/update/package.html
@@ -0,0 +1,8 @@
+<html>
+<body>
+<p>
+Interfaces for classes to assist in the update process without
+needing the router context.
+</p>
+</body>
+</html>
diff --git a/core/java/src/net/i2p/util/VersionComparator.java b/core/java/src/net/i2p/util/VersionComparator.java
index 3b6e97f5b4..8c72252fcb 100644
--- a/core/java/src/net/i2p/util/VersionComparator.java
+++ b/core/java/src/net/i2p/util/VersionComparator.java
@@ -22,7 +22,7 @@ public class VersionComparator implements Comparator<String> {
         while (lTokens.hasMoreTokens() && rTokens.hasMoreTokens()) {
             String lNumber = lTokens.nextToken();
             String rNumber = rTokens.nextToken();
-            int diff = intCompare(lNumber, rNumber);
+            int diff = longCompare(lNumber, rNumber);
             if (diff != 0)
                 return diff;
         }
@@ -34,19 +34,24 @@ public class VersionComparator implements Comparator<String> {
         return 0;
     }
 
-    private static final int intCompare(String lop, String rop) {
-        int left, right;
+    private static final int longCompare(String lop, String rop) {
+        long left, right;
         try {
-            left = Integer.parseInt(lop);
+            left = Long.parseLong(lop);
         } catch (NumberFormatException nfe) {
             return -1;
         }
         try {
-            right = Integer.parseInt(rop);
+            right = Long.parseLong(rop);
         } catch (NumberFormatException nfe) {
             return 1;
         }
-        return left - right;
+        long diff = left - right;
+        if (diff < 0)
+            return -1;
+        if (diff > 0)
+            return 1;
+        return 0;
     }
 
     private static final String VALID_SEPARATOR_CHARS = ".-_";
diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java
index baf00b5b1e..30b3ba3e26 100644
--- a/router/java/src/net/i2p/router/RouterContext.java
+++ b/router/java/src/net/i2p/router/RouterContext.java
@@ -22,6 +22,7 @@ import net.i2p.router.transport.FIFOBandwidthLimiter;
 import net.i2p.router.transport.OutboundMessageRegistry;
 import net.i2p.router.tunnel.TunnelDispatcher;
 import net.i2p.router.tunnel.pool.TunnelPoolManager;
+import net.i2p.update.UpdateManager;
 import net.i2p.util.KeyRing;
 import net.i2p.util.I2PProperties.I2PPropertyCallback;
 
@@ -55,11 +56,12 @@ public class RouterContext extends I2PAppContext {
     private Shitlist _shitlist;
     private Blocklist _blocklist;
     private MessageValidator _messageValidator;
+    private UpdateManager _updateManager;
     //private MessageStateMonitor _messageStateMonitor;
     private RouterThrottle _throttle;
     private final Set<Runnable> _finalShutdownTasks;
     // split up big lock on this to avoid deadlocks
-    private final Object _lock1 = new Object(), _lock2 = new Object();
+    private final Object _lock1 = new Object(), _lock2 = new Object(), _lock3 = new Object();
 
     private static List<RouterContext> _contexts = new ArrayList(1);
     
@@ -481,6 +483,7 @@ public class RouterContext extends I2PAppContext {
      *  @return true
      *  @since 0.7.9
      */
+    @Override
     public boolean isRouterContext() {
         return true;
     }
@@ -490,7 +493,44 @@ public class RouterContext extends I2PAppContext {
      *  @return the client manager
      *  @since 0.8.3
      */
+    @Override
     public InternalClientManager internalClientManager() {
         return _clientManagerFacade;
     }
+
+    /**
+     *  The controller of router, plugin, and other updates.
+     *  @return The manager if it is registered, else null
+     *  @since 0.9.2
+     */
+    @Override
+    public UpdateManager updateManager() {
+        return _updateManager;
+    }
+
+    /**
+     *  Register as the update manager.
+     *  @throws IllegalStateException if one was already registered
+     *  @since 0.9.2
+     */
+    public void registerUpdateManager(UpdateManager mgr) {
+        synchronized(_lock3) {
+            if (_updateManager != null)
+                throw new IllegalStateException();
+            _updateManager = mgr;
+        }
+    }
+
+    /**
+     *  Unregister the update manager.
+     *  @throws IllegalStateException if it was not registered
+     *  @since 0.9.2
+     */
+    public void unregisterUpdateManager(UpdateManager mgr) {
+        synchronized(_lock3) {
+            if (_updateManager != mgr)
+                throw new IllegalStateException();
+            _updateManager = null;
+        }
+    }
 }
-- 
GitLab