diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/PendingContactState.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/PendingContactState.java index 9fdb1344289b80c79ee2b18ad51a40bc7106d6fb..60ed7731863de22d99759efa077a7fee60c4cecc 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/PendingContactState.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/PendingContactState.java @@ -3,6 +3,7 @@ package org.briarproject.bramble.api.contact; public enum PendingContactState { WAITING_FOR_CONNECTION, + OFFLINE, CONNECTING, ADDING_CONTACT, FAILED diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java index f3138fd75fc6c6065f29d23430e93a7dbd4312eb..a03206ad2619faf5a22e6574a670472e015dd2a1 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java @@ -147,7 +147,6 @@ class ContactManagerImpl implements ContactManager, EventListener { } finally { db.endTransaction(txn); } - states.put(p.getId(), WAITING_FOR_CONNECTION); return p; } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/rendezvous/RendezvousPollerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/rendezvous/RendezvousPollerImpl.java index 6f5ec72b70fed1c1b1306dd3b51510b8e81ba8be..d16317bffbe6b17ac74a26143f2f487939127b50 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/rendezvous/RendezvousPollerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/rendezvous/RendezvousPollerImpl.java @@ -66,6 +66,7 @@ import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.api.contact.PendingContactState.ADDING_CONTACT; import static org.briarproject.bramble.api.contact.PendingContactState.FAILED; +import static org.briarproject.bramble.api.contact.PendingContactState.OFFLINE; import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNull; import static org.briarproject.bramble.rendezvous.RendezvousConstants.POLLING_INTERVAL_MS; @@ -158,9 +159,7 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener { private void addPendingContact(PendingContact p) { long now = clock.currentTimeMillis(); long expiry = p.getTimestamp() + RENDEZVOUS_TIMEOUT_MS; - if (expiry > now) { - broadcastState(p.getId(), WAITING_FOR_CONNECTION); - } else { + if (expiry <= now) { broadcastState(p.getId(), FAILED); return; } @@ -180,9 +179,13 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener { for (PluginState ps : pluginStates.values()) { RendezvousEndpoint endpoint = createEndpoint(ps.plugin, p.getId(), cs); - if (endpoint != null) + if (endpoint != null) { requireNull(ps.endpoints.put(p.getId(), endpoint)); + cs.numEndpoints++; + } } + if (cs.numEndpoints == 0) broadcastState(p.getId(), OFFLINE); + else broadcastState(p.getId(), WAITING_FOR_CONNECTION); } catch (DbException | GeneralSecurityException e) { logException(LOG, WARNING, e); } @@ -328,9 +331,14 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener { TransportId t = plugin.getId(); Map<PendingContactId, RendezvousEndpoint> endpoints = new HashMap<>(); for (Entry<PendingContactId, CryptoState> e : cryptoStates.entrySet()) { - RendezvousEndpoint endpoint = - createEndpoint(plugin, e.getKey(), e.getValue()); - if (endpoint != null) endpoints.put(e.getKey(), endpoint); + PendingContactId p = e.getKey(); + CryptoState cs = e.getValue(); + RendezvousEndpoint endpoint = createEndpoint(plugin, p, cs); + if (endpoint != null) { + endpoints.put(p, endpoint); + if (++cs.numEndpoints == 1) + broadcastState(p, WAITING_FOR_CONNECTION); + } } requireNull(pluginStates.put(t, new PluginState(plugin, endpoints))); } @@ -344,8 +352,11 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener { private void removeTransport(TransportId t) { PluginState ps = pluginStates.remove(t); if (ps != null) { - for (RendezvousEndpoint endpoint : ps.endpoints.values()) { - tryToClose(endpoint, LOG, INFO); + for (Entry<PendingContactId, RendezvousEndpoint> e : + ps.endpoints.entrySet()) { + tryToClose(e.getValue(), LOG, INFO); + CryptoState cs = cryptoStates.get(e.getKey()); + if (--cs.numEndpoints == 0) broadcastState(e.getKey(), OFFLINE); } } } @@ -391,6 +402,8 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener { private final boolean alice; private final long expiry; + private int numEndpoints = 0; + private CryptoState(SecretKey rendezvousKey, boolean alice, long expiry) { this.rendezvousKey = rendezvousKey; diff --git a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTest.java index 0409571f9f38d22827731edc8ec2988ec90abd88..4bf7ae48d3acbaae3b3d711c60a4b9128839a95d 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTest.java @@ -6,11 +6,15 @@ import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.PendingContact; import org.briarproject.bramble.api.contact.PendingContactState; import org.briarproject.bramble.api.contact.event.ContactAddedEvent; +import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent; import org.briarproject.bramble.api.crypto.PublicKey; import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.identity.Identity; import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.test.BrambleTestCase; import org.briarproject.bramble.test.TestDatabaseConfigModule; import org.briarproject.bramble.test.TestDuplexTransportConnection; @@ -27,7 +31,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertNull; import static junit.framework.TestCase.fail; -import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION; +import static org.briarproject.bramble.api.contact.PendingContactState.OFFLINE; import static org.briarproject.bramble.test.TestDuplexTransportConnection.createPair; import static org.briarproject.bramble.test.TestPluginConfigModule.DUPLEX_TRANSPORT_ID; import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory; @@ -188,9 +192,14 @@ public class ContactExchangeIntegrationTest extends BrambleTestCase { private PendingContact addPendingContact( ContactExchangeIntegrationTestComponent local, ContactExchangeIntegrationTestComponent remote) throws Exception { + EventWaiter waiter = new EventWaiter(); + local.getEventBus().addListener(waiter); String link = remote.getContactManager().getHandshakeLink(); String alias = remote.getIdentityManager().getLocalAuthor().getName(); - return local.getContactManager().addPendingContact(link, alias); + PendingContact pendingContact = + local.getContactManager().addPendingContact(link, alias); + waiter.latch.await(TIMEOUT, MILLISECONDS); + return pendingContact; } private void assertContacts(boolean verified, @@ -237,7 +246,7 @@ public class ContactExchangeIntegrationTest extends BrambleTestCase { assertEquals(1, pairs.size()); Pair<PendingContact, PendingContactState> pair = pairs.iterator().next(); - assertEquals(WAITING_FOR_CONNECTION, pair.getSecond()); + assertEquals(OFFLINE, pair.getSecond()); PendingContact pendingContact = pair.getFirst(); assertEquals(expectedIdentity.getLocalAuthor().getName(), pendingContact.getAlias()); @@ -261,4 +270,19 @@ public class ContactExchangeIntegrationTest extends BrambleTestCase { tearDown(bob); deleteTestDirectory(testDir); } + + @NotNullByDefault + private static class EventWaiter implements EventListener { + + private final CountDownLatch latch = new CountDownLatch(1); + + @Override + public void eventOccurred(Event e) { + if (e instanceof PendingContactStateChangedEvent) { + PendingContactStateChangedEvent p = + (PendingContactStateChangedEvent) e; + if (p.getPendingContactState() == OFFLINE) latch.countDown(); + } + } + } } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/rendezvous/RendezvousPollerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/rendezvous/RendezvousPollerImplTest.java index 27321d748e9f23470c0662c8ed8e8c213f37647d..0f65d9adf5ca8f139d5ca42ac03d0844df22f41a 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/rendezvous/RendezvousPollerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/rendezvous/RendezvousPollerImplTest.java @@ -45,6 +45,7 @@ import static java.util.Collections.singletonList; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.briarproject.bramble.api.contact.PendingContactState.ADDING_CONTACT; import static org.briarproject.bramble.api.contact.PendingContactState.FAILED; +import static org.briarproject.bramble.api.contact.PendingContactState.OFFLINE; import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION; import static org.briarproject.bramble.rendezvous.RendezvousConstants.POLLING_INTERVAL_MS; import static org.briarproject.bramble.rendezvous.RendezvousConstants.RENDEZVOUS_TIMEOUT_MS; @@ -120,7 +121,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { will(returnValue(beforeExpiry)); oneOf(eventBus).broadcast(with(new PredicateMatcher<>( PendingContactStateChangedEvent.class, e -> - e.getPendingContactState() == WAITING_FOR_CONNECTION))); + e.getPendingContactState() == OFFLINE))); // Capture the poll task oneOf(scheduler).scheduleAtFixedRate(with(any(Runnable.class)), with(POLLING_INTERVAL_MS), with(POLLING_INTERVAL_MS), @@ -184,7 +185,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { context.assertIsSatisfied(); // Add the pending contact - endpoint should be created and polled - expectAddUnexpiredPendingContact(beforeExpiry); + expectAddPendingContact(beforeExpiry, WAITING_FOR_CONNECTION); expectDeriveRendezvousKey(); expectCreateEndpoint(); @@ -205,9 +206,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { context.assertIsSatisfied(); // Remove the pending contact - endpoint should be closed - context.checking(new Expectations() {{ - oneOf(rendezvousEndpoint).close(); - }}); + expectCloseEndpoint(); rendezvousPoller.eventOccurred( new PendingContactRemovedEvent(pendingContact.getId())); @@ -238,7 +237,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { context.assertIsSatisfied(); // Add the pending contact - endpoint should be created and polled - expectAddUnexpiredPendingContact(beforeExpiry); + expectAddPendingContact(beforeExpiry, WAITING_FOR_CONNECTION); expectDeriveRendezvousKey(); expectCreateEndpoint(); @@ -260,10 +259,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { // Run the poll task - pending contact expires, endpoint is closed expectPendingContactExpires(afterExpiry); - - context.checking(new Expectations() {{ - oneOf(rendezvousEndpoint).close(); - }}); + expectCloseEndpoint(); capturePollTask.get().run(); context.assertIsSatisfied(); @@ -289,7 +285,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { context.assertIsSatisfied(); // Add the pending contact - no endpoints should be created yet - expectAddUnexpiredPendingContact(beforeExpiry); + expectAddPendingContact(beforeExpiry, OFFLINE); expectDeriveRendezvousKey(); rendezvousPoller.eventOccurred( @@ -299,14 +295,14 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { // Enable the transport - endpoint should be created expectGetPlugin(); expectCreateEndpoint(); + expectStateChangedEvent(WAITING_FOR_CONNECTION); rendezvousPoller.eventOccurred(new TransportEnabledEvent(transportId)); context.assertIsSatisfied(); // Disable the transport - endpoint should be closed - context.checking(new Expectations() {{ - oneOf(rendezvousEndpoint).close(); - }}); + expectCloseEndpoint(); + expectStateChangedEvent(OFFLINE); rendezvousPoller.eventOccurred(new TransportDisabledEvent(transportId)); context.assertIsSatisfied(); @@ -482,13 +478,14 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { return capturePollTask; } - private void expectAddUnexpiredPendingContact(long now) { + private void expectAddPendingContact(long now, + PendingContactState initialState) { context.checking(new Expectations() {{ oneOf(clock).currentTimeMillis(); will(returnValue(now)); oneOf(eventBus).broadcast(with(new PredicateMatcher<>( PendingContactStateChangedEvent.class, e -> - e.getPendingContactState() == WAITING_FOR_CONNECTION))); + e.getPendingContactState() == initialState))); }}); } @@ -546,7 +543,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { will(returnValue(now)); oneOf(eventBus).broadcast(with(new PredicateMatcher<>( PendingContactStateChangedEvent.class, e -> - e.getPendingContactState() == WAITING_FOR_CONNECTION))); + e.getPendingContactState() == OFFLINE))); // Capture the poll task oneOf(scheduler).scheduleAtFixedRate(with(any(Runnable.class)), with(POLLING_INTERVAL_MS), with(POLLING_INTERVAL_MS), @@ -576,4 +573,10 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { e.getPendingContactState() == state))); }}); } + + private void expectCloseEndpoint() throws Exception { + context.checking(new Expectations() {{ + oneOf(rendezvousEndpoint).close(); + }}); + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactViewHolder.java index 980ef3bec013dfbb340954b169f617ce89e9c4a4..f1abd03f20535df3aa1fa45ca31180e7ffe3abbe 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactViewHolder.java @@ -52,6 +52,11 @@ class PendingContactViewHolder extends ViewHolder { .getColor(status.getContext(), R.color.briar_yellow); status.setText(R.string.waiting_for_contact_to_come_online); break; + case OFFLINE: + color = ContextCompat + .getColor(status.getContext(), R.color.briar_yellow); + status.setText(""); + break; case CONNECTING: status.setText(R.string.connecting); break; diff --git a/briar-headless/README.md b/briar-headless/README.md index 19004e25accecfcb361579b1008b6060437ffff6..59bcc64c37630af42a4761fedc98a620acfbc56c 100644 --- a/briar-headless/README.md +++ b/briar-headless/README.md @@ -131,6 +131,7 @@ This will return a JSON array of pending contacts and their states: The state can be one of these values: * `waiting_for_connection` + * `offline` * `connecting` * `adding_contact` * `failed` diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputPendingContact.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputPendingContact.kt index b6226b627511d9aee80825280df6642ecec767a3..429edb44f82c14d23802a10183089705ef3deba8 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputPendingContact.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputPendingContact.kt @@ -16,6 +16,7 @@ internal fun PendingContact.output() = JsonDict( internal fun PendingContactState.output() = when(this) { WAITING_FOR_CONNECTION -> "waiting_for_connection" + OFFLINE -> "offline" CONNECTING -> "connecting" ADDING_CONTACT -> "adding_contact" FAILED -> "failed"