From 47c91a96ae80ff5a118a7aec89b72bf10781988d Mon Sep 17 00:00:00 2001
From: akwizgran <michael@briarproject.org>
Date: Wed, 10 Oct 2018 13:40:36 +0100
Subject: [PATCH] Compact the database at startup.

---
 .../briarproject/bramble/api/StringMap.java   | 14 ++++
 .../bramble/api/db/MigrationListener.java     |  6 +-
 .../api/lifecycle/LifecycleManager.java       |  3 +-
 .../bramble/test/SettableClock.java           | 24 +++++++
 .../bramble/db/DatabaseConstants.java         | 14 ++++
 .../briarproject/bramble/db/H2Database.java   | 19 ++++++
 .../bramble/db/HyperSqlDatabase.java          | 26 ++++++-
 .../briarproject/bramble/db/JdbcDatabase.java | 67 ++++++++++++++++---
 .../lifecycle/LifecycleManagerImpl.java       |  9 ++-
 .../bramble/db/JdbcDatabaseTest.java          | 17 ++---
 .../android/login/OpenDatabaseActivity.java   | 13 +++-
 briar-android/src/main/res/values/strings.xml |  1 +
 12 files changed, 191 insertions(+), 22 deletions(-)
 create mode 100644 bramble-api/src/test/java/org/briarproject/bramble/test/SettableClock.java

diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/StringMap.java b/bramble-api/src/main/java/org/briarproject/bramble/api/StringMap.java
index aeb9823a17..bda2a94d24 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/StringMap.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/StringMap.java
@@ -38,4 +38,18 @@ public abstract class StringMap extends Hashtable<String, String> {
 	public void putInt(String key, int value) {
 		put(key, String.valueOf(value));
 	}
+
+	public long getLong(String key, long defaultValue) {
+		String s = get(key);
+		if (s == null) return defaultValue;
+		try {
+			return Long.valueOf(s);
+		} catch (NumberFormatException e) {
+			return defaultValue;
+		}
+	}
+
+	public void putLong(String key, long value) {
+		put(key, String.valueOf(value));
+	}
 }
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/db/MigrationListener.java b/bramble-api/src/main/java/org/briarproject/bramble/api/db/MigrationListener.java
index 79e292a75f..92c9bf1638 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/db/MigrationListener.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/db/MigrationListener.java
@@ -6,6 +6,10 @@ public interface MigrationListener {
 	 * This is called when a migration is started while opening the database.
 	 * It will be called once for each migration being applied.
 	 */
-	void onMigrationRun();
+	void onDatabaseMigration();
 
+	/**
+	 * This is called when compaction is started while opening the database.
+	 */
+	void onDatabaseCompaction();
 }
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java
index c44cf8879e..2ef340a872 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java
@@ -34,7 +34,8 @@ public interface LifecycleManager {
 	 */
 	enum LifecycleState {
 
-		STARTING, MIGRATING_DATABASE, STARTING_SERVICES, RUNNING, STOPPING;
+		STARTING, MIGRATING_DATABASE, COMPACTING_DATABASE, STARTING_SERVICES,
+		RUNNING, STOPPING;
 
 		public boolean isAfter(LifecycleState state) {
 			return ordinal() > state.ordinal();
diff --git a/bramble-api/src/test/java/org/briarproject/bramble/test/SettableClock.java b/bramble-api/src/test/java/org/briarproject/bramble/test/SettableClock.java
new file mode 100644
index 0000000000..26f885de80
--- /dev/null
+++ b/bramble-api/src/test/java/org/briarproject/bramble/test/SettableClock.java
@@ -0,0 +1,24 @@
+package org.briarproject.bramble.test;
+
+import org.briarproject.bramble.api.system.Clock;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+public class SettableClock implements Clock {
+
+	private final AtomicLong time;
+
+	public SettableClock(AtomicLong time) {
+		this.time = time;
+	}
+
+	@Override
+	public long currentTimeMillis() {
+		return time.get();
+	}
+
+	@Override
+	public void sleep(long milliseconds) throws InterruptedException {
+		Thread.sleep(milliseconds);
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseConstants.java b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseConstants.java
index 707234b1ff..f27d45cedd 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseConstants.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseConstants.java
@@ -2,6 +2,8 @@ package org.briarproject.bramble.db;
 
 import org.briarproject.bramble.api.settings.Settings;
 
+import static java.util.concurrent.TimeUnit.DAYS;
+
 interface DatabaseConstants {
 
 	/**
@@ -23,4 +25,16 @@ interface DatabaseConstants {
 	 */
 	String SCHEMA_VERSION_KEY = "schemaVersion";
 
+	/**
+	 * The {@link Settings} key under which the time of the last database
+	 * compaction is stored.
+	 */
+	String LAST_COMPACTED_KEY = "lastCompacted";
+
+	/**
+	 * The maximum time between database compactions in milliseconds. When the
+	 * database is opened it will be compacted if more than this amount of time
+	 * has passed since the last compaction.
+	 */
+	long MAX_COMPACTION_INTERVAL_MS = DAYS.toMillis(30);
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java b/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java
index 028312d81d..85bebb67c5 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java
@@ -13,6 +13,7 @@ import java.io.File;
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.SQLException;
+import java.sql.Statement;
 import java.util.Properties;
 
 import javax.annotation.Nullable;
@@ -106,4 +107,22 @@ class H2Database extends JdbcDatabase {
 	String getUrl() {
 		return url;
 	}
+
+	@Override
+	protected void compactAndClose() throws DbException {
+		Connection c = null;
+		Statement s = null;
+		try {
+			c = createConnection();
+			closeAllConnections();
+			s = c.createStatement();
+			s.execute("SHUTDOWN COMPACT");
+			s.close();
+			c.close();
+		} catch (SQLException e) {
+			tryToClose(s);
+			tryToClose(c);
+			throw new DbException(e);
+		}
+	}
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java
index f5419f7cb4..e8ba7b33dc 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java
@@ -61,14 +61,18 @@ class HyperSqlDatabase extends JdbcDatabase {
 
 	@Override
 	public void close() throws DbException {
+		Connection c = null;
+		Statement s = null;
 		try {
 			super.closeAllConnections();
-			Connection c = createConnection();
-			Statement s = c.createStatement();
+			c = createConnection();
+			s = c.createStatement();
 			s.executeQuery("SHUTDOWN");
 			s.close();
 			c.close();
 		} catch (SQLException e) {
+			tryToClose(s);
+			tryToClose(c);
 			throw new DbException(e);
 		}
 	}
@@ -104,4 +108,22 @@ class HyperSqlDatabase extends JdbcDatabase {
 		String hex = StringUtils.toHexString(key.getBytes());
 		return DriverManager.getConnection(url + ";crypt_key=" + hex);
 	}
+
+	@Override
+	protected void compactAndClose() throws DbException {
+		Connection c = null;
+		Statement s = null;
+		try {
+			super.closeAllConnections();
+			c = createConnection();
+			s = c.createStatement();
+			s.executeQuery("SHUTDOWN COMPACT");
+			s.close();
+			c.close();
+		} catch (SQLException e) {
+			tryToClose(s);
+			tryToClose(c);
+			throw new DbException(e);
+		}
+	}
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
index 2396ee9077..307da37d7a 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
@@ -67,9 +67,13 @@ import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERE
 import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
 import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
 import static org.briarproject.bramble.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
+import static org.briarproject.bramble.db.DatabaseConstants.LAST_COMPACTED_KEY;
+import static org.briarproject.bramble.db.DatabaseConstants.MAX_COMPACTION_INTERVAL_MS;
 import static org.briarproject.bramble.db.DatabaseConstants.SCHEMA_VERSION_KEY;
 import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
+import static org.briarproject.bramble.util.LogUtils.logDuration;
 import static org.briarproject.bramble.util.LogUtils.logException;
+import static org.briarproject.bramble.util.LogUtils.now;
 
 /**
  * A generic database implementation that can be used with any JDBC-compatible
@@ -317,9 +321,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 	private int openConnections = 0; // Locking: connectionsLock
 	private boolean closed = false; // Locking: connectionsLock
 
-	@Nullable
 	protected abstract Connection createConnection() throws SQLException;
 
+	protected abstract void compactAndClose() throws DbException;
+
 	private final Lock connectionsLock = new ReentrantLock();
 	private final Condition connectionsChanged = connectionsLock.newCondition();
 
@@ -344,13 +349,21 @@ abstract class JdbcDatabase implements Database<Connection> {
 			throw new DbException(e);
 		}
 		// Open the database and create the tables and indexes if necessary
+		boolean compact;
 		Connection txn = startTransaction();
 		try {
 			if (reopen) {
-				checkSchemaVersion(txn, listener);
+				Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
+				checkSchemaVersion(txn, s, listener);
+				long lastCompacted = s.getLong(LAST_COMPACTED_KEY, 0);
+				long elapsed = clock.currentTimeMillis() - lastCompacted;
+				if (LOG.isLoggable(INFO))
+					LOG.info(elapsed + " ms since last compaction");
+				compact = elapsed > MAX_COMPACTION_INTERVAL_MS;
 			} else {
 				createTables(txn);
-				storeSchemaVersion(txn, CODE_SCHEMA_VERSION);
+				initialiseSettings(txn);
+				compact = false;
 			}
 			createIndexes(txn);
 			commitTransaction(txn);
@@ -358,6 +371,25 @@ abstract class JdbcDatabase implements Database<Connection> {
 			abortTransaction(txn);
 			throw e;
 		}
+		// Compact the database if necessary
+		if (compact) {
+			if (listener != null) listener.onDatabaseCompaction();
+			long start = now();
+			compactAndClose();
+			logDuration(LOG, "Compacting database", start);
+			// Allow the next transaction to reopen the DB
+			synchronized (connectionsLock) {
+				closed = false;
+			}
+			txn = startTransaction();
+			try {
+				storeLastCompacted(txn);
+				commitTransaction(txn);
+			} catch (DbException e) {
+				abortTransaction(txn);
+				throw e;
+			}
+		}
 	}
 
 	/**
@@ -370,9 +402,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 	 * @throws DataTooOldException if the data uses an older schema than the
 	 * current code and cannot be migrated
 	 */
-	private void checkSchemaVersion(Connection txn,
+	private void checkSchemaVersion(Connection txn, Settings s,
 			@Nullable MigrationListener listener) throws DbException {
-		Settings s = getSettings(txn, DB_SETTINGS_NAMESPACE);
 		int dataSchemaVersion = s.getInt(SCHEMA_VERSION_KEY, -1);
 		if (dataSchemaVersion == -1) throw new DbException();
 		if (dataSchemaVersion == CODE_SCHEMA_VERSION) return;
@@ -384,7 +415,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if (start == dataSchemaVersion) {
 				if (LOG.isLoggable(INFO))
 					LOG.info("Migrating from schema " + start + " to " + end);
-				if (listener != null) listener.onMigrationRun();
+				if (listener != null) listener.onDatabaseMigration();
 				// Apply the migration
 				m.migrate(txn);
 				// Store the new schema version
@@ -408,6 +439,19 @@ abstract class JdbcDatabase implements Database<Connection> {
 		mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
 	}
 
+	private void storeLastCompacted(Connection txn) throws DbException {
+		Settings s = new Settings();
+		s.putLong(LAST_COMPACTED_KEY, clock.currentTimeMillis());
+		mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
+	}
+
+	private void initialiseSettings(Connection txn) throws DbException {
+		Settings s = new Settings();
+		s.putInt(SCHEMA_VERSION_KEY, CODE_SCHEMA_VERSION);
+		s.putLong(LAST_COMPACTED_KEY, clock.currentTimeMillis());
+		mergeSettings(txn, s, DB_SETTINGS_NAMESPACE);
+	}
+
 	private void tryToClose(@Nullable ResultSet rs) {
 		try {
 			if (rs != null) rs.close();
@@ -416,7 +460,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	private void tryToClose(@Nullable Statement s) {
+	protected void tryToClose(@Nullable Statement s) {
 		try {
 			if (s != null) s.close();
 		} catch (SQLException e) {
@@ -424,6 +468,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	protected void tryToClose(@Nullable Connection c) {
+		try {
+			if (c != null) c.close();
+		} catch (SQLException e) {
+			logException(LOG, WARNING, e);
+		}
+	}
+
 	private void createTables(Connection txn) throws DbException {
 		Statement s = null;
 		try {
@@ -489,7 +541,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if (txn == null) {
 				// Open a new connection
 				txn = createConnection();
-				if (txn == null) throw new DbException();
 				txn.setAutoCommit(false);
 				connectionsLock.lock();
 				try {
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java
index 130595ab63..f158a690f0 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java
@@ -29,6 +29,7 @@ import javax.inject.Inject;
 import static java.util.logging.Level.FINE;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE;
 import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE;
 import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
 import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING;
@@ -159,11 +160,17 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
 	}
 
 	@Override
-	public void onMigrationRun() {
+	public void onDatabaseMigration() {
 		state = MIGRATING_DATABASE;
 		eventBus.broadcast(new LifecycleEvent(MIGRATING_DATABASE));
 	}
 
+	@Override
+	public void onDatabaseCompaction() {
+		state = COMPACTING_DATABASE;
+		eventBus.broadcast(new LifecycleEvent(COMPACTING_DATABASE));
+	}
+
 	@Override
 	public void stopServices() {
 		try {
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
index 9e6a0d2731..94eb4d1969 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
@@ -26,8 +26,8 @@ import org.briarproject.bramble.api.transport.KeySetId;
 import org.briarproject.bramble.api.transport.OutgoingKeys;
 import org.briarproject.bramble.api.transport.TransportKeys;
 import org.briarproject.bramble.system.SystemClock;
-import org.briarproject.bramble.test.ArrayClock;
 import org.briarproject.bramble.test.BrambleTestCase;
+import org.briarproject.bramble.test.SettableClock;
 import org.briarproject.bramble.test.TestDatabaseConfig;
 import org.briarproject.bramble.test.TestMessageFactory;
 import org.briarproject.bramble.test.TestUtils;
@@ -46,6 +46,7 @@ import java.util.Map.Entry;
 import java.util.Random;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
 
 import static java.util.Collections.emptyList;
 import static java.util.Collections.emptyMap;
@@ -1818,10 +1819,9 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 	@Test
 	public void testMessageRetransmission() throws Exception {
 		long now = System.currentTimeMillis();
-		long steps[] = {now, now, now + MAX_LATENCY * 2 - 1,
-				now + MAX_LATENCY * 2};
+		AtomicLong time = new AtomicLong(now);
 		Database<Connection> db =
-				open(false, new TestMessageFactory(), new ArrayClock(steps));
+				open(false, new TestMessageFactory(), new SettableClock(time));
 		Connection txn = db.startTransaction();
 
 		// Add a contact, a shared group and a shared message
@@ -1847,11 +1847,13 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Time: now + MAX_LATENCY * 2 - 1
 		// The message should not yet be sendable
+		time.set(now + MAX_LATENCY * 2 - 1);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertTrue(ids.isEmpty());
 
 		// Time: now + MAX_LATENCY * 2
 		// The message should have expired and should now be sendable
+		time.set(now + MAX_LATENCY * 2);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
 		assertEquals(singletonList(messageId), ids);
 
@@ -1859,13 +1861,12 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		db.close();
 	}
 
-
 	@Test
 	public void testFasterMessageRetransmission() throws Exception {
 		long now = System.currentTimeMillis();
-		long steps[] = {now, now, now, now, now + 1};
+		AtomicLong time = new AtomicLong(now);
 		Database<Connection> db =
-				open(false, new TestMessageFactory(), new ArrayClock(steps));
+				open(false, new TestMessageFactory(), new SettableClock(time));
 		Connection txn = db.startTransaction();
 
 		// Add a contact, a shared group and a shared message
@@ -1903,6 +1904,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Time: now + 1
 		// The message should no longer be sendable via the faster transport,
 		// as the ETA is now equal
+		time.set(now + 1);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE,
 				MAX_LATENCY - 1);
 		assertTrue(ids.isEmpty());
@@ -1911,7 +1913,6 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		db.close();
 	}
 
-
 	private Database<Connection> open(boolean resume) throws Exception {
 		return open(resume, new TestMessageFactory(), new SystemClock());
 	}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/login/OpenDatabaseActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/login/OpenDatabaseActivity.java
index d24196d32b..24ccc1e010 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/login/OpenDatabaseActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/login/OpenDatabaseActivity.java
@@ -20,6 +20,7 @@ import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
 import javax.annotation.ParametersAreNonnullByDefault;
 import javax.inject.Inject;
 
+import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE;
 import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE;
 import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES;
 
@@ -34,7 +35,7 @@ public class OpenDatabaseActivity extends BriarActivity
 
 	private TextView textView;
 	private ImageView imageView;
-	private boolean showingMigration = false;
+	private boolean showingMigration = false, showingCompaction = false;
 
 	@Override
 	public void onCreate(@Nullable Bundle state) {
@@ -57,6 +58,7 @@ public class OpenDatabaseActivity extends BriarActivity
 			finishAndStartApp();
 		} else {
 			if (state == MIGRATING_DATABASE) showMigration();
+			else if (state == COMPACTING_DATABASE) showCompaction();
 			eventBus.addListener(this);
 		}
 	}
@@ -75,6 +77,8 @@ public class OpenDatabaseActivity extends BriarActivity
 				runOnUiThreadUnlessDestroyed(this::finishAndStartApp);
 			else if (state == MIGRATING_DATABASE)
 				runOnUiThreadUnlessDestroyed(this::showMigration);
+			else if (state == COMPACTING_DATABASE)
+				runOnUiThreadUnlessDestroyed(this::showCompaction);
 		}
 	}
 
@@ -85,6 +89,13 @@ public class OpenDatabaseActivity extends BriarActivity
 		showingMigration = true;
 	}
 
+	private void showCompaction() {
+		if (showingCompaction) return;
+		textView.setText(R.string.startup_compact_database);
+		imageView.setImageResource(R.drawable.startup_migration);
+		showingCompaction = true;
+	}
+
 	private void finishAndStartApp() {
 		startActivity(new Intent(this, NavDrawerActivity.class));
 		supportFinishAfterTransition();
diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml
index 08afd7f570..4875cc50c4 100644
--- a/briar-android/src/main/res/values/strings.xml
+++ b/briar-android/src/main/res/values/strings.xml
@@ -54,6 +54,7 @@
 	<string name="download_briar_button">Download Briar 1.0</string>
 	<string name="startup_open_database">Decrypting Database…</string>
 	<string name="startup_migrate_database">Upgrading Database…</string>
+	<string name="startup_compact_database">Compacting Database…</string>
 
 	<!-- Navigation Drawer -->
 	<string name="nav_drawer_open_description">Open the navigation drawer</string>
-- 
GitLab