diff --git a/src/java/com/threerings/util/IdleTracker.java b/src/java/com/threerings/util/IdleTracker.java
new file mode 100644
index 000000000..a3529246d
--- /dev/null
+++ b/src/java/com/threerings/util/IdleTracker.java
@@ -0,0 +1,173 @@
+//
+// $Id$
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.util;
+
+import java.awt.AWTEvent;
+import java.awt.Toolkit;
+import java.awt.event.AWTEventListener;
+
+import com.samskivert.util.Interval;
+import com.samskivert.util.RunQueue;
+
+/**
+ * Used to track user idleness in an AWT application.
+ */
+public abstract class IdleTracker
+{
+ /**
+ * Creates an idle tracker that will report idleness (via {@link
+ * #idledOut}) after toIdleTime milliseconds have elapsed.
+ * After an additional toAbandonTime milliseconds have
+ * elapsed, we will report that the user has {@link #abandonedShip}.
+ */
+ public IdleTracker (long toIdleTime, long toAbandonTime)
+ {
+ _toIdleTime = toIdleTime;
+ _toAbandonTime = toAbandonTime;
+
+ // initialize our last event time
+ _lastEvent = getTimeStamp();
+ }
+
+ public void start (KeyboardManager keymgr, RunQueue rqueue)
+ {
+ // we want to observe all mouse and keyboard events
+ long eventMask =
+ AWTEvent.MOUSE_EVENT_MASK |
+ AWTEvent.MOUSE_MOTION_EVENT_MASK |
+ AWTEvent.MOUSE_WHEEL_EVENT_MASK |
+ AWTEvent.KEY_EVENT_MASK;
+
+ // add the global event listener
+ Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
+ public void eventDispatched (AWTEvent event) {
+ handleUserActivity();
+ }
+ }, eventMask);
+
+ // and tie into the keyboard manager as well
+ keymgr.registerKeyObserver(new KeyboardManager.KeyObserver() {
+ public void handleKeyEvent (int id, int keyCode, long timestamp) {
+ handleUserActivity();
+ }
+ });
+
+ // register an interval to periodically check our last activity time
+ new Interval(rqueue) {
+ public void expired () {
+ checkIdle();
+ }
+ }.schedule(IDLE_INTERVAL, true);
+ }
+
+ /**
+ * Called when the client has been idle for {@link #_toIdleTime}
+ * milliseconds.
+ */
+ protected abstract void idledOut ();
+
+ /**
+ * Called when the client becomes non-idle after we have previously
+ * reported their idleness.
+ */
+ protected abstract void idledIn ();
+
+ /**
+ * Called when the client has been idle for {@link #_toIdleTime} plus
+ * {@link #_toAbandonTime} milliseconds.
+ */
+ protected abstract void abandonedShip ();
+
+ /**
+ * This should return a timestamp. We would use {@link
+ * System#getCurrentTimeMillis} except that on Windows that sometimes does
+ * strange things like leap forward in time causing immediate idleness.
+ */
+ protected abstract long getTimeStamp ();
+
+ /**
+ * Called with any keyboard or mouse events performed on the frame so
+ * as to note user activity as it pertains to tracking the client idle
+ * state.
+ */
+ protected void handleUserActivity ()
+ {
+ // note the time of the last user action
+ _lastEvent = getTimeStamp();
+
+ // idle-in if appropriate
+ if (_state != State.ACTIVE) {
+ _state = State.ACTIVE;
+ idledIn();
+ }
+ }
+
+ /**
+ * Checks the last user event time and posts a command to idle them
+ * out if they've been inactive for too long, or log them out if
+ * they've been idle for too long.
+ */
+ protected void checkIdle ()
+ {
+ long now = getTimeStamp();
+
+ switch (_state) {
+ case ACTIVE:
+ // check whether they've idled out
+ if (now >= (_lastEvent + _toIdleTime)) {
+ Log.info("User idle for " + (now-_lastEvent) + "ms.");
+ _state = State.IDLE;
+ idledOut();
+ }
+ break;
+
+ case IDLE:
+ // check whether they've been idle for too long
+ if (now >= (_lastEvent + _toIdleTime + _toAbandonTime)) {
+ Log.info("User idle for " + (now-_lastEvent) + "ms. " +
+ "Abandoning ship.");
+ _state = State.ABANDONED;
+ abandonedShip();
+ }
+ break;
+ }
+ }
+
+ /** The user's current state. */
+ protected static enum State { ACTIVE, IDLE, ABANDONED };
+
+ /** The duration after which we declare the user to be idle. */
+ protected long _toIdleTime;
+
+ /** The duration after which we declare the user to have abandoned ship. */
+ protected long _toAbandonTime;
+
+ /** The time of the last mouse or keyboard event; used to track
+ * whether the user is idle. */
+ protected long _lastEvent;
+
+ /** Whether the user is currently active, idle or abandoned. */
+ protected State _state;
+
+ /** The delay in milliseconds between checks for user idle. */
+ protected static final long IDLE_INTERVAL = 60L * 1000L;
+}