diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ConnectivityChecker.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ConnectivityChecker.java
new file mode 100644
index 000000000..921557027
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ConnectivityChecker.java
@@ -0,0 +1,32 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An interface for checking whether a mailbox is reachable.
+ */
+@ThreadSafe
+@NotNullByDefault
+interface ConnectivityChecker {
+
+ /**
+ * Destroys the checker. Any current connectivity check is cancelled.
+ */
+ void destroy();
+
+ /**
+ * Starts a connectivity check if needed and calls the given observer when
+ * the check succeeds. If a check is already running then the observer is
+ * called when the check succeeds. If a connectivity check has recently
+ * succeeded then the observer is called immediately.
+ */
+ void checkConnectivity(MailboxProperties properties,
+ ConnectivityObserver o);
+
+ interface ConnectivityObserver {
+ void onConnectivityCheckSucceeded();
+ }
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ConnectivityCheckerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ConnectivityCheckerImpl.java
new file mode 100644
index 000000000..1468c5db1
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ConnectivityCheckerImpl.java
@@ -0,0 +1,111 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.Cancellable;
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.system.Clock;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+@ThreadSafe
+@NotNullByDefault
+abstract class ConnectivityCheckerImpl implements ConnectivityChecker {
+
+ /**
+ * If no more than this much time has elapsed since the last connectivity
+ * check succeeded, consider the result to be fresh and don't check again.
+ *
+ * Package access for testing.
+ */
+ static final long CONNECTIVITY_CHECK_FRESHNESS_MS = 10_000;
+
+ private final Object lock = new Object();
+
+ protected final Clock clock;
+ private final MailboxApiCaller mailboxApiCaller;
+
+ @GuardedBy("lock")
+ private boolean destroyed = false;
+
+ @GuardedBy("lock")
+ @Nullable
+ private Cancellable connectivityCheck = null;
+
+ @GuardedBy("lock")
+ private long lastConnectivityCheckSucceeded = 0;
+
+ @GuardedBy("lock")
+ private final List connectivityObservers =
+ new ArrayList<>();
+
+ /**
+ * Creates an {@link ApiCall} for checking whether the mailbox is
+ * reachable. The {@link ApiCall} should call
+ * {@link #onConnectivityCheckSucceeded(long)} if the check succeeds.
+ */
+ abstract ApiCall createConnectivityCheckTask(MailboxProperties properties);
+
+ ConnectivityCheckerImpl(Clock clock, MailboxApiCaller mailboxApiCaller) {
+ this.clock = clock;
+ this.mailboxApiCaller = mailboxApiCaller;
+ }
+
+ @Override
+ public void destroy() {
+ synchronized (lock) {
+ destroyed = true;
+ connectivityObservers.clear();
+ if (connectivityCheck != null) {
+ connectivityCheck.cancel();
+ connectivityCheck = null;
+ }
+ }
+ }
+
+ @Override
+ public void checkConnectivity(MailboxProperties properties,
+ ConnectivityObserver o) {
+ boolean callNow = false;
+ synchronized (lock) {
+ if (destroyed) return;
+ if (connectivityCheck == null) {
+ // No connectivity check is running
+ long now = clock.currentTimeMillis();
+ if (now - lastConnectivityCheckSucceeded
+ > CONNECTIVITY_CHECK_FRESHNESS_MS) {
+ // The last connectivity check is stale, start a new one
+ connectivityObservers.add(o);
+ ApiCall task =
+ createConnectivityCheckTask(properties);
+ connectivityCheck = mailboxApiCaller.retryWithBackoff(task);
+ } else {
+ // The last connectivity check is fresh
+ callNow = true;
+ }
+ } else {
+ // A connectivity check is running, wait for it to succeed
+ connectivityObservers.add(o);
+ }
+ }
+ if (callNow) o.onConnectivityCheckSucceeded();
+ }
+
+ protected void onConnectivityCheckSucceeded(long now) {
+ List observers;
+ synchronized (lock) {
+ if (destroyed) return;
+ connectivityCheck = null;
+ lastConnectivityCheckSucceeded = now;
+ observers = new ArrayList<>(connectivityObservers);
+ connectivityObservers.clear();
+ }
+ for (ConnectivityObserver o : observers) {
+ o.onConnectivityCheckSucceeded();
+ }
+ }
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ContactMailboxConnectivityChecker.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ContactMailboxConnectivityChecker.java
new file mode 100644
index 000000000..03ca2e257
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ContactMailboxConnectivityChecker.java
@@ -0,0 +1,40 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
+
+import java.io.IOException;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
+
+@ThreadSafe
+@NotNullByDefault
+class ContactMailboxConnectivityChecker extends ConnectivityCheckerImpl {
+
+ private final MailboxApi mailboxApi;
+
+ ContactMailboxConnectivityChecker(Clock clock,
+ MailboxApiCaller mailboxApiCaller, MailboxApi mailboxApi) {
+ super(clock, mailboxApiCaller);
+ this.mailboxApi = mailboxApi;
+ }
+
+ @Override
+ ApiCall createConnectivityCheckTask(MailboxProperties properties) {
+ if (properties.isOwner()) throw new IllegalArgumentException();
+ return new SimpleApiCall() {
+ @Override
+ void tryToCallApi() throws IOException, ApiException {
+ mailboxApi.getFiles(properties,
+ requireNonNull(properties.getInboxId()));
+ // Call the observers and cache the result
+ onConnectivityCheckSucceeded(clock.currentTimeMillis());
+ }
+ };
+ }
+
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityChecker.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityChecker.java
new file mode 100644
index 000000000..af6b12264
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityChecker.java
@@ -0,0 +1,70 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.db.TransactionManager;
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
+
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.bramble.util.LogUtils.logException;
+
+@ThreadSafe
+@NotNullByDefault
+class OwnMailboxConnectivityChecker extends ConnectivityCheckerImpl {
+
+ private static final Logger LOG =
+ getLogger(OwnMailboxConnectivityChecker.class.getName());
+
+ private final MailboxApi mailboxApi;
+ private final TransactionManager db;
+ private final MailboxSettingsManager mailboxSettingsManager;
+
+ OwnMailboxConnectivityChecker(Clock clock,
+ MailboxApiCaller mailboxApiCaller,
+ MailboxApi mailboxApi,
+ TransactionManager db,
+ MailboxSettingsManager mailboxSettingsManager) {
+ super(clock, mailboxApiCaller);
+ this.mailboxApi = mailboxApi;
+ this.db = db;
+ this.mailboxSettingsManager = mailboxSettingsManager;
+ }
+
+ @Override
+ ApiCall createConnectivityCheckTask(MailboxProperties properties) {
+ if (!properties.isOwner()) throw new IllegalArgumentException();
+ return () -> {
+ try {
+ try {
+ mailboxApi.getFolders(properties);
+ LOG.info("Own mailbox is reachable");
+ long now = clock.currentTimeMillis();
+ db.transaction(false, txn -> mailboxSettingsManager
+ .recordSuccessfulConnection(txn, now));
+ // Call the observers and cache the result
+ onConnectivityCheckSucceeded(now);
+ return false; // Don't retry
+ } catch (IOException | ApiException e) {
+ LOG.warning("Own mailbox is unreachable");
+ logException(LOG, WARNING, e);
+ long now = clock.currentTimeMillis();
+ db.transaction(false, txn -> mailboxSettingsManager
+ .recordFailedConnectionAttempt(txn, now));
+ }
+ } catch (DbException e) {
+ logException(LOG, WARNING, e);
+ }
+ return true; // Retry
+ };
+ }
+
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/ConnectivityCheckerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/ConnectivityCheckerImplTest.java
new file mode 100644
index 000000000..448b36ea9
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/ConnectivityCheckerImplTest.java
@@ -0,0 +1,201 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.Cancellable;
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
+import org.briarproject.bramble.test.BrambleMockTestCase;
+import org.jmock.Expectations;
+import org.junit.Test;
+
+import javax.annotation.Nonnull;
+
+import static org.briarproject.bramble.mailbox.ConnectivityCheckerImpl.CONNECTIVITY_CHECK_FRESHNESS_MS;
+import static org.briarproject.bramble.mailbox.MailboxApi.CLIENT_SUPPORTS;
+import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
+
+public class ConnectivityCheckerImplTest extends BrambleMockTestCase {
+
+ private final Clock clock = context.mock(Clock.class);
+ private final MailboxApiCaller mailboxApiCaller =
+ context.mock(MailboxApiCaller.class);
+ private final ApiCall apiCall = context.mock(ApiCall.class);
+ private final Cancellable task = context.mock(Cancellable.class);
+ private final ConnectivityObserver observer1 =
+ context.mock(ConnectivityObserver.class, "1");
+ private final ConnectivityObserver observer2 =
+ context.mock(ConnectivityObserver.class, "2");
+
+ private final MailboxProperties properties =
+ getMailboxProperties(true, CLIENT_SUPPORTS);
+ private final long now = System.currentTimeMillis();
+
+ @Test
+ public void testFirstObserverStartsCheck() {
+ ConnectivityCheckerImpl checker = createChecker();
+
+ // When checkConnectivity() is called a check should be started
+ context.checking(new Expectations() {{
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now));
+ oneOf(mailboxApiCaller).retryWithBackoff(apiCall);
+ will(returnValue(task));
+ }});
+
+ checker.checkConnectivity(properties, observer1);
+
+ // When the check succeeds the observer should be called
+ context.checking(new Expectations() {{
+ oneOf(observer1).onConnectivityCheckSucceeded();
+ }});
+
+ checker.onConnectivityCheckSucceeded(now);
+
+ // The observer should not be called again when subsequent checks
+ // succeed
+ checker.onConnectivityCheckSucceeded(now);
+ }
+
+ @Test
+ public void testObserverIsAddedToExistingCheck() {
+ ConnectivityCheckerImpl checker = createChecker();
+
+ // When checkConnectivity() is called a check should be started
+ context.checking(new Expectations() {{
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now));
+ oneOf(mailboxApiCaller).retryWithBackoff(apiCall);
+ will(returnValue(task));
+ }});
+
+ checker.checkConnectivity(properties, observer1);
+
+ // When checkConnectivity() is called again before the first check
+ // succeeds, the observer should be added to the existing check
+ checker.checkConnectivity(properties, observer2);
+
+ // When the check succeeds both observers should be called
+ context.checking(new Expectations() {{
+ oneOf(observer1).onConnectivityCheckSucceeded();
+ oneOf(observer2).onConnectivityCheckSucceeded();
+ }});
+
+ checker.onConnectivityCheckSucceeded(now);
+
+ // The observers should not be called again when subsequent checks
+ // succeed
+ checker.onConnectivityCheckSucceeded(now);
+ }
+
+ @Test
+ public void testFreshResultIsReused() {
+ ConnectivityCheckerImpl checker = createChecker();
+
+ // When checkConnectivity() is called a check should be started
+ context.checking(new Expectations() {{
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now));
+ oneOf(mailboxApiCaller).retryWithBackoff(apiCall);
+ will(returnValue(task));
+ }});
+
+ checker.checkConnectivity(properties, observer1);
+
+ // When the check succeeds the observer should be called
+ context.checking(new Expectations() {{
+ oneOf(observer1).onConnectivityCheckSucceeded();
+ }});
+
+ checker.onConnectivityCheckSucceeded(now);
+
+ // When checkConnectivity() is called again within
+ // CONNECTIVITY_CHECK_FRESHNESS_MS the observer should be called with
+ // the result of the recent check
+ context.checking(new Expectations() {{
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now + CONNECTIVITY_CHECK_FRESHNESS_MS));
+ oneOf(observer2).onConnectivityCheckSucceeded();
+ }});
+
+ checker.checkConnectivity(properties, observer2);
+ }
+
+ @Test
+ public void testStaleResultIsNotReused() {
+ ConnectivityCheckerImpl checker = createChecker();
+
+ // When checkConnectivity() is called a check should be started
+ context.checking(new Expectations() {{
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now));
+ oneOf(mailboxApiCaller).retryWithBackoff(apiCall);
+ will(returnValue(task));
+ }});
+
+ checker.checkConnectivity(properties, observer1);
+
+ // When the check succeeds the observer should be called
+ context.checking(new Expectations() {{
+ oneOf(observer1).onConnectivityCheckSucceeded();
+ }});
+
+ checker.onConnectivityCheckSucceeded(now);
+
+ // When checkConnectivity() is called again after more than
+ // CONNECTIVITY_CHECK_FRESHNESS_MS another check should be started
+ context.checking(new Expectations() {{
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now + CONNECTIVITY_CHECK_FRESHNESS_MS + 1));
+ oneOf(mailboxApiCaller).retryWithBackoff(apiCall);
+ will(returnValue(task));
+ }});
+
+ checker.checkConnectivity(properties, observer2);
+
+ // When the check succeeds the observer should be called
+ context.checking(new Expectations() {{
+ oneOf(observer2).onConnectivityCheckSucceeded();
+ }});
+
+ checker.onConnectivityCheckSucceeded(
+ now + CONNECTIVITY_CHECK_FRESHNESS_MS + 1);
+ }
+
+ @Test
+ public void testCheckIsCancelledWhenCheckerIsDestroyed() {
+ ConnectivityCheckerImpl checker = createChecker();
+
+ // When checkConnectivity() is called a check should be started
+ context.checking(new Expectations() {{
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now));
+ oneOf(mailboxApiCaller).retryWithBackoff(apiCall);
+ will(returnValue(task));
+ }});
+
+ checker.checkConnectivity(properties, observer1);
+
+ // When the checker is destroyed the check should be cancelled
+ context.checking(new Expectations() {{
+ oneOf(task).cancel();
+ }});
+
+ checker.destroy();
+
+ // If the check runs anyway (cancellation came too late) the observer
+ // should not be called
+ checker.onConnectivityCheckSucceeded(now);
+ }
+
+ private ConnectivityCheckerImpl createChecker() {
+
+ return new ConnectivityCheckerImpl(clock, mailboxApiCaller) {
+ @Override
+ @Nonnull
+ protected ApiCall createConnectivityCheckTask(
+ @Nonnull MailboxProperties properties) {
+ return apiCall;
+ }
+ };
+ }
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/ContactMailboxConnectivityCheckerTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/ContactMailboxConnectivityCheckerTest.java
new file mode 100644
index 000000000..222226c00
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/ContactMailboxConnectivityCheckerTest.java
@@ -0,0 +1,102 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.Cancellable;
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
+import org.briarproject.bramble.test.BrambleMockTestCase;
+import org.briarproject.bramble.test.CaptureArgumentAction;
+import org.jmock.Expectations;
+import org.jmock.lib.action.DoAllAction;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static java.util.Collections.emptyList;
+import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
+import static org.briarproject.bramble.mailbox.MailboxApi.CLIENT_SUPPORTS;
+import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class ContactMailboxConnectivityCheckerTest extends BrambleMockTestCase {
+
+ private final Clock clock = context.mock(Clock.class);
+ private final MailboxApiCaller mailboxApiCaller =
+ context.mock(MailboxApiCaller.class);
+ private final MailboxApi mailboxApi = context.mock(MailboxApi.class);
+ private final Cancellable task = context.mock(Cancellable.class);
+ private final ConnectivityObserver observer =
+ context.mock(ConnectivityObserver.class);
+
+ private final MailboxProperties properties =
+ getMailboxProperties(false, CLIENT_SUPPORTS);
+ private final long now = System.currentTimeMillis();
+
+ @Test
+ public void testObserverIsCalledWhenCheckSucceeds() throws Exception {
+ ContactMailboxConnectivityChecker checker = createChecker();
+ AtomicReference apiCall = new AtomicReference<>(null);
+
+ // When checkConnectivity() is called a check should be started
+ context.checking(new Expectations() {{
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now));
+ oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
+ will(new DoAllAction(
+ new CaptureArgumentAction<>(apiCall, ApiCall.class, 0),
+ returnValue(task)
+ ));
+ }});
+
+ checker.checkConnectivity(properties, observer);
+
+ // When the check succeeds the observer should be called
+ context.checking(new Expectations() {{
+ oneOf(mailboxApi).getFiles(properties,
+ requireNonNull(properties.getInboxId()));
+ will(returnValue(emptyList()));
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now));
+ oneOf(observer).onConnectivityCheckSucceeded();
+ }});
+
+ // The call should not be retried
+ assertFalse(apiCall.get().callApi());
+ }
+
+ @Test
+ public void testObserverIsNotCalledWhenCheckFails() throws Exception {
+ ContactMailboxConnectivityChecker checker = createChecker();
+ AtomicReference apiCall = new AtomicReference<>(null);
+
+ // When checkConnectivity() is called a check should be started
+ context.checking(new Expectations() {{
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now));
+ oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
+ will(new DoAllAction(
+ new CaptureArgumentAction<>(apiCall, ApiCall.class, 0),
+ returnValue(task)
+ ));
+ }});
+
+ checker.checkConnectivity(properties, observer);
+
+ // When the check fails, the observer should not be called
+ context.checking(new Expectations() {{
+ oneOf(mailboxApi).getFiles(properties,
+ requireNonNull(properties.getInboxId()));
+ will(throwException(new IOException()));
+ }});
+
+ // The call should be retried
+ assertTrue(apiCall.get().callApi());
+ }
+
+ private ContactMailboxConnectivityChecker createChecker() {
+ return new ContactMailboxConnectivityChecker(clock, mailboxApiCaller,
+ mailboxApi);
+ }
+}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityCheckerTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityCheckerTest.java
new file mode 100644
index 000000000..2f4678b25
--- /dev/null
+++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityCheckerTest.java
@@ -0,0 +1,118 @@
+package org.briarproject.bramble.mailbox;
+
+import org.briarproject.bramble.api.Cancellable;
+import org.briarproject.bramble.api.db.Transaction;
+import org.briarproject.bramble.api.db.TransactionManager;
+import org.briarproject.bramble.api.mailbox.MailboxProperties;
+import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
+import org.briarproject.bramble.api.system.Clock;
+import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
+import org.briarproject.bramble.test.BrambleMockTestCase;
+import org.briarproject.bramble.test.CaptureArgumentAction;
+import org.briarproject.bramble.test.DbExpectations;
+import org.jmock.Expectations;
+import org.jmock.lib.action.DoAllAction;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static java.util.Collections.emptyList;
+import static org.briarproject.bramble.mailbox.MailboxApi.CLIENT_SUPPORTS;
+import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class OwnMailboxConnectivityCheckerTest extends BrambleMockTestCase {
+
+ private final Clock clock = context.mock(Clock.class);
+ private final MailboxApiCaller mailboxApiCaller =
+ context.mock(MailboxApiCaller.class);
+ private final MailboxApi mailboxApi = context.mock(MailboxApi.class);
+ private final TransactionManager db =
+ context.mock(TransactionManager.class);
+ private final MailboxSettingsManager mailboxSettingsManager =
+ context.mock(MailboxSettingsManager.class);
+ private final Cancellable task = context.mock(Cancellable.class);
+ private final ConnectivityObserver observer =
+ context.mock(ConnectivityObserver.class);
+
+ private final MailboxProperties properties =
+ getMailboxProperties(true, CLIENT_SUPPORTS);
+ private final long now = System.currentTimeMillis();
+
+ @Test
+ public void testObserverIsCalledWhenCheckSucceeds() throws Exception {
+ OwnMailboxConnectivityChecker checker = createChecker();
+ AtomicReference apiCall = new AtomicReference<>(null);
+ Transaction txn = new Transaction(null, false);
+
+ // When checkConnectivity() is called a check should be started
+ context.checking(new Expectations() {{
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now));
+ oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
+ will(new DoAllAction(
+ new CaptureArgumentAction<>(apiCall, ApiCall.class, 0),
+ returnValue(task)
+ ));
+ }});
+
+ checker.checkConnectivity(properties, observer);
+
+ // When the check succeeds, the success should be recorded in the DB
+ // and the observer should be called
+ context.checking(new DbExpectations() {{
+ oneOf(mailboxApi).getFolders(properties);
+ will(returnValue(emptyList()));
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now));
+ oneOf(db).transaction(with(false), withDbRunnable(txn));
+ oneOf(mailboxSettingsManager).recordSuccessfulConnection(txn, now);
+ oneOf(observer).onConnectivityCheckSucceeded();
+ }});
+
+ // The call should not be retried
+ assertFalse(apiCall.get().callApi());
+ }
+
+ @Test
+ public void testObserverIsNotCalledWhenCheckFails() throws Exception {
+ OwnMailboxConnectivityChecker checker = createChecker();
+ AtomicReference apiCall = new AtomicReference<>(null);
+ Transaction txn = new Transaction(null, false);
+
+ // When checkConnectivity() is called a check should be started
+ context.checking(new Expectations() {{
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now));
+ oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
+ will(new DoAllAction(
+ new CaptureArgumentAction<>(apiCall, ApiCall.class, 0),
+ returnValue(task)
+ ));
+ }});
+
+ checker.checkConnectivity(properties, observer);
+
+ // When the check fails, the failure should be recorded in the DB and
+ // the observer should not be called
+ context.checking(new DbExpectations() {{
+ oneOf(mailboxApi).getFolders(properties);
+ will(throwException(new IOException()));
+ oneOf(clock).currentTimeMillis();
+ will(returnValue(now));
+ oneOf(db).transaction(with(false), withDbRunnable(txn));
+ oneOf(mailboxSettingsManager)
+ .recordFailedConnectionAttempt(txn, now);
+ }});
+
+ // The call should be retried
+ assertTrue(apiCall.get().callApi());
+ }
+
+ private OwnMailboxConnectivityChecker createChecker() {
+ return new OwnMailboxConnectivityChecker(clock, mailboxApiCaller,
+ mailboxApi, db, mailboxSettingsManager);
+ }
+}