diff --git a/briar-api/src/net/sf/briar/api/Contact.java b/briar-api/src/net/sf/briar/api/Contact.java
index cfe5ddf14573063efabc91f4d913a6ce90a6fffc..bbac1dc99bdebb7d720cf4fd63586e7adf72321a 100644
--- a/briar-api/src/net/sf/briar/api/Contact.java
+++ b/briar-api/src/net/sf/briar/api/Contact.java
@@ -4,10 +4,12 @@ public class Contact {
 
 	private final ContactId id;
 	private final String name;
+	private final long lastConnected;
 
-	public Contact(ContactId id, String name) {
+	public Contact(ContactId id, String name, long lastConnected) {
 		this.id = id;
 		this.name = name;
+		this.lastConnected = lastConnected;
 	}
 
 	public ContactId getId() {
@@ -18,6 +20,10 @@ public class Contact {
 		return name;
 	}
 
+	public long getLastConnected() {
+		return lastConnected;
+	}
+
 	@Override
 	public int hashCode() {
 		return id.hashCode();
diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java
index 34fe4ab8ccd3f57652d9ce549921fb733617787d..43a91fa56b4de64d43c89ea211f321685319f095 100644
--- a/briar-core/src/net/sf/briar/db/Database.java
+++ b/briar-core/src/net/sf/briar/db/Database.java
@@ -81,7 +81,8 @@ interface Database<T> {
 	 * Adds a contact with the given name to the database and returns an ID for
 	 * the contact.
 	 * <p>
-	 * Locking: contact write, subscription write.
+	 * Locking: contact write, retention write, subscription write, transport
+	 * write, window write.
 	 */
 	ContactId addContact(T txn, String name) throws DbException;
 
@@ -210,7 +211,7 @@ interface Database<T> {
 	/**
 	 * Returns all contacts.
 	 * <p>
-	 * Locking: contact read.
+	 * Locking: contact read, window read.
 	 */
 	Collection<Contact> getContacts(T txn) throws DbException;
 
@@ -239,6 +240,14 @@ interface Database<T> {
 	 */
 	MessageId getGroupMessageParent(T txn, MessageId m) throws DbException;
 
+	/**
+	 * Returns the time at which a connection to the given contact was last
+	 * opened or closed.
+	 * <p>
+	 * Locking: contact read, window read.
+	 */
+	long getLastConnected(T txn, ContactId c) throws DbException;
+
 	/**
 	 * Returns the local transport properties for the given transport.
 	 * <p>
@@ -526,8 +535,8 @@ interface Database<T> {
 	/**
 	 * Removes a contact (and all associated state) from the database.
 	 * <p>
-	 * Locking: contact write, message write, subscription write,
-	 * transport write, window write.
+	 * Locking: contact write, message write, retention write,
+	 * subscription write, transport write, window write.
 	 */
 	void removeContact(T txn, ContactId c) throws DbException;
 
@@ -587,6 +596,14 @@ interface Database<T> {
 	void setConnectionWindow(T txn, ContactId c, TransportId t, long period,
 			long centre, byte[] bitmap) throws DbException;
 
+	/**
+	 * Sets the time at which a connection to the given contact was last
+	 * opened or closed.
+	 * <p>
+	 * Locking: contact read, window write.
+	 */
+	void setLastConnected(T txn, ContactId c, long now) throws DbException;
+
 	/**
 	 * Sets the user's rating for the given author.
 	 * <p>
diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
index 789b37bd563288dd61b4a723e901cea6075f1d5c..c4455971a9c5b4da12764d185b100f1ecd26209a 100644
--- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
@@ -176,18 +176,33 @@ DatabaseCleaner.Callback {
 		ContactId c;
 		contactLock.writeLock().lock();
 		try {
-			subscriptionLock.writeLock().lock();
+			retentionLock.writeLock().lock();
 			try {
-				T txn = db.startTransaction();
+				subscriptionLock.writeLock().lock();
 				try {
-					c = db.addContact(txn, name);
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
+					transportLock.writeLock().lock();
+					try {
+						windowLock.writeLock().lock();
+						try {
+							T txn = db.startTransaction();
+							try {
+								c = db.addContact(txn, name);
+								db.commitTransaction(txn);
+							} catch(DbException e) {
+								db.abortTransaction(txn);
+								throw e;
+							}
+						} finally {
+							windowLock.writeLock().unlock();
+						}
+					} finally {
+						transportLock.writeLock().unlock();
+					}
+				} finally {
+					subscriptionLock.writeLock().unlock();
 				}
 			} finally {
-				subscriptionLock.writeLock().unlock();
+				retentionLock.writeLock().unlock();
 			}
 		} finally {
 			contactLock.writeLock().unlock();
@@ -805,14 +820,19 @@ DatabaseCleaner.Callback {
 	public Collection<Contact> getContacts() throws DbException {
 		contactLock.readLock().lock();
 		try {
-			T txn = db.startTransaction();
+			windowLock.readLock().lock();
 			try {
-				Collection<Contact> contacts = db.getContacts(txn);
-				db.commitTransaction(txn);
-				return contacts;
-			} catch(DbException e) {
-				db.abortTransaction(txn);
-				throw e;
+				T txn = db.startTransaction();
+				try {
+					Collection<Contact> contacts = db.getContacts(txn);
+					db.commitTransaction(txn);
+					return contacts;
+				} catch(DbException e) {
+					db.abortTransaction(txn);
+					throw e;
+				}
+			} finally {
+				windowLock.readLock().unlock();
 			}
 		} finally {
 			contactLock.readLock().unlock();
@@ -1140,6 +1160,7 @@ DatabaseCleaner.Callback {
 							throw new NoSuchTransportException();
 						long counter = db.incrementConnectionCounter(txn, c, t,
 								period);
+						db.setLastConnected(txn, c, clock.currentTimeMillis());
 						db.commitTransaction(txn);
 						return counter;
 					} catch(DbException e) {
@@ -1467,30 +1488,35 @@ DatabaseCleaner.Callback {
 		try {
 			messageLock.writeLock().lock();
 			try {
-				subscriptionLock.writeLock().lock();
+				retentionLock.writeLock().lock();
 				try {
-					transportLock.writeLock().lock();
+					subscriptionLock.writeLock().lock();
 					try {
-						windowLock.writeLock().lock();
+						transportLock.writeLock().lock();
 						try {
-							T txn = db.startTransaction();
+							windowLock.writeLock().lock();
 							try {
-								if(!db.containsContact(txn, c))
-									throw new NoSuchContactException();
-								db.removeContact(txn, c);
-								db.commitTransaction(txn);
-							} catch(DbException e) {
-								db.abortTransaction(txn);
-								throw e;
+								T txn = db.startTransaction();
+								try {
+									if(!db.containsContact(txn, c))
+										throw new NoSuchContactException();
+									db.removeContact(txn, c);
+									db.commitTransaction(txn);
+								} catch(DbException e) {
+									db.abortTransaction(txn);
+									throw e;
+								}
+							} finally {
+								windowLock.writeLock().unlock();
 							}
 						} finally {
-							windowLock.writeLock().unlock();
+							transportLock.writeLock().unlock();
 						}
 					} finally {
-						transportLock.writeLock().unlock();
+						subscriptionLock.writeLock().unlock();
 					}
 				} finally {
-					subscriptionLock.writeLock().unlock();
+					retentionLock.writeLock().unlock();
 				}
 			} finally {
 				messageLock.writeLock().unlock();
@@ -1536,6 +1562,7 @@ DatabaseCleaner.Callback {
 							throw new NoSuchTransportException();
 						db.setConnectionWindow(txn, c, t, period, centre,
 								bitmap);
+						db.setLastConnected(txn, c, clock.currentTimeMillis());
 						db.commitTransaction(txn);
 					} catch(DbException e) {
 						db.abortTransaction(txn);
diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
index dabf3df2b7988fd7aa7edb631ae9120976da62ec..f907d7a37bd483aaf4c2b6e0c8d1a513cf8c3bf1 100644
--- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java
+++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
@@ -305,6 +305,16 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " REFERENCES transports (transportId)"
 					+ " ON DELETE CASCADE)";
 
+	// Locking: contact read, window
+	private static final String CREATE_CONNECTION_TIMES =
+			"CREATE TABLE connectionTimes"
+					+ " (contactId INT NOT NULL,"
+					+ " lastConnected BIGINT NOT NULL,"
+					+ " PRIMARY KEY (contactId),"
+					+ " FOREIGN KEY (contactId)"
+					+ " REFERENCES contacts (contactId)"
+					+ " ON DELETE CASCADE)";
+
 	private static final Logger LOG =
 			Logger.getLogger(JdbcDatabase.class.getName());
 
@@ -383,6 +393,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			s.executeUpdate(insertTypeNames(CREATE_CONTACT_TRANSPORT_VERSIONS));
 			s.executeUpdate(insertTypeNames(CREATE_ENDPOINTS));
 			s.executeUpdate(insertTypeNames(CREATE_SECRETS));
+			s.executeUpdate(insertTypeNames(CREATE_CONNECTION_TIMES));
 			s.close();
 		} catch(SQLException e) {
 			tryToClose(s);
@@ -519,6 +530,15 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if(rs.next()) throw new DbStateException();
 			rs.close();
 			ps.close();
+			// Create a connection time row
+			sql = "INSERT INTO connectionTimes (contactId, lastConnected)"
+					+ " VALUES (?, ?)";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.setLong(2, clock.currentTimeMillis());
+			affected = ps.executeUpdate();
+			if(affected != 1) throw new DbStateException();
+			ps.close();
 			// Create a retention version row
 			sql = "INSERT INTO retentionVersions (contactId, retention,"
 					+ " localVersion, localAcked, remoteVersion, remoteAcked,"
@@ -1032,14 +1052,18 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT contactId, name FROM contacts";
+			String sql = "SELECT contactId, name, lastConnected"
+					+ " FROM contacts AS c"
+					+ " JOIN connectionTimes AS ct"
+					+ " ON c.contactId = ct.contactId";
 			ps = txn.prepareStatement(sql);
 			rs = ps.executeQuery();
 			List<Contact> contacts = new ArrayList<Contact>();
 			while(rs.next()) {
 				ContactId id = new ContactId(rs.getInt(1));
 				String name = rs.getString(2);
-				contacts.add(new Contact(id, name));
+				long lastConnected = rs.getLong(3);
+				contacts.add(new Contact(id, name, lastConnected));
 			}
 			rs.close();
 			ps.close();
@@ -1116,6 +1140,29 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public long getLastConnected(Connection txn, ContactId c)
+			throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT lastConnected FROM connectionTimes"
+					+ " WHERE contactId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			rs = ps.executeQuery();
+			if(!rs.next()) throw new DbStateException();
+			long lastConnected = rs.getLong(1);
+			if(rs.next()) throw new DbStateException();
+			rs.close();
+			ps.close();
+			return lastConnected;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public TransportProperties getLocalProperties(Connection txn, TransportId t)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -2508,6 +2555,24 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public void setLastConnected(Connection txn, ContactId c, long now)
+			throws DbException {
+		PreparedStatement ps = null;
+		try {
+			String sql = "UPDATE connectionTimes SET lastConnected = ?"
+					+ " WHERE contactId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setLong(1, now);
+			ps.setInt(2, c.getInt());
+			int affected = ps.executeUpdate();
+			if(affected < 1) throw new DbStateException();
+			ps.close();
+		} catch(SQLException e) {
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public Rating setRating(Connection txn, AuthorId a, Rating r)
 			throws DbException {
 		PreparedStatement ps = null;
diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
index f035ed314ffa5faa85a79be7b1a6586c0ba28ea9..db67d27c52133d62df2fb83d19809eeb1a6457f1 100644
--- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
+++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
@@ -81,7 +81,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		timestamp = System.currentTimeMillis();
 		size = 1234;
 		raw = new byte[size];
-		contact = new Contact(contactId, contactName);
+		contact = new Contact(contactId, contactName, timestamp);
 		message = new TestMessage(messageId, null, groupId, authorId, subject,
 				timestamp, raw);
 		privateMessage = new TestMessage(messageId, null, null, null, subject,