Merge branch '295-show-declined-introductions' into 'master'

Show relevant introduction decline responses in the conversation

* If the user has already declined, we don't show that the other
  introducee has declined as well. The backend doesn't have that information, so
  this is compatible with the principle of showing what we know.
* If the user has already accepted or hasn't yet responded, we now show the
  decline response in the private conversation with the introducer. If
  the user hasn't yet responded, we hide the accept/decline buttons
  in the introduction request message.

Please note that I do not have three devices at the moment to test this MR in its entirety in practice. I created another test which is hopefully sufficient to ensure that the modifications are correct.

Closes #295 

See merge request !149
This commit is contained in:
akwizgran
2016-04-25 12:07:13 +00:00
11 changed files with 219 additions and 54 deletions

View File

@@ -50,6 +50,7 @@ import javax.inject.Inject;
import static org.briarproject.TestPluginsModule.MAX_LATENCY;
import static org.briarproject.TestPluginsModule.TRANSPORT_ID;
import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -293,7 +294,19 @@ public class IntroductionIntegrationTest extends BriarTestCase {
assertFalse(contactManager2
.contactExists(author1.getId(), author2.getId()));
assertDefaultUiMessages();
assertEquals(2,
introductionManager0.getIntroductionMessages(contactId1)
.size());
assertEquals(2,
introductionManager0.getIntroductionMessages(contactId2)
.size());
assertEquals(2,
introductionManager1.getIntroductionMessages(contactId0)
.size());
// introducee2 should also have the decline response of introducee1
assertEquals(3,
introductionManager2.getIntroductionMessages(contactId0)
.size());
} finally {
stopLifecycles();
}
@@ -369,6 +382,97 @@ public class IntroductionIntegrationTest extends BriarTestCase {
assertFalse(contactManager2
.contactExists(author1.getId(), author2.getId()));
assertEquals(2,
introductionManager0.getIntroductionMessages(contactId1)
.size());
assertEquals(2,
introductionManager0.getIntroductionMessages(contactId2)
.size());
// introducee1 also sees the decline response from introducee2
assertEquals(3,
introductionManager1.getIntroductionMessages(contactId0)
.size());
assertEquals(2,
introductionManager2.getIntroductionMessages(contactId0)
.size());
} finally {
stopLifecycles();
}
}
@Test
public void testIntroductionSessionDelayedFirstDecline() throws Exception {
startLifecycles();
try {
// Add Identities
addDefaultIdentities();
// Add Transport Properties
addTransportProperties();
// Add introducees as contacts
contactId1 = contactManager0.addContact(author1, author0.getId(),
master, clock.currentTimeMillis(), true, true
);
contactId2 = contactManager0.addContact(author2, author0.getId(),
master, clock.currentTimeMillis(), true, true
);
// Add introducer back
contactId0 = contactManager1.addContact(author0, author1.getId(),
master, clock.currentTimeMillis(), false, true
);
ContactId contactId02 = contactManager2.addContact(author0,
author2.getId(), master, clock.currentTimeMillis(), false,
true
);
assertTrue(contactId0.equals(contactId02));
// listen to events
IntroducerListener listener0 = new IntroducerListener();
t0.getEventBus().addListener(listener0);
IntroduceeListener listener1 = new IntroduceeListener(1, false);
t1.getEventBus().addListener(listener1);
IntroduceeListener listener2 = new IntroduceeListener(2, false);
t2.getEventBus().addListener(listener2);
// make introduction
long time = clock.currentTimeMillis();
Contact introducee1 = contactManager0.getContact(contactId1);
Contact introducee2 = contactManager0.getContact(contactId2);
introductionManager0
.makeIntroduction(introducee1, introducee2, null, time);
// sync request messages
deliverMessage(sync0, contactId0, sync1, contactId1);
deliverMessage(sync0, contactId0, sync2, contactId2);
// wait for requests to arrive
eventWaiter.await(TIMEOUT, 2);
assertTrue(listener1.requestReceived);
assertTrue(listener2.requestReceived);
// sync first response
deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0");
eventWaiter.await(TIMEOUT, 1);
assertTrue(listener0.response1Received);
// sync second response
deliverMessage(sync2, contactId2, sync0, contactId0, "2 to 0");
eventWaiter.await(TIMEOUT, 1);
assertTrue(listener0.response2Received);
// sync first forwarded response
deliverMessage(sync0, contactId0, sync2, contactId2);
// note how the second response will not be forwarded anymore
assertFalse(contactManager1
.contactExists(author2.getId(), author1.getId()));
assertFalse(contactManager2
.contactExists(author1.getId(), author2.getId()));
// since introducee2 was already in FINISHED state when
// introducee1's response arrived, she ignores and deletes it
assertDefaultUiMessages();
} finally {
stopLifecycles();
@@ -614,14 +718,18 @@ public class IntroductionIntegrationTest extends BriarTestCase {
}
private void assertDefaultUiMessages() throws DbException {
assertTrue(introductionManager0.getIntroductionMessages(contactId1)
.size() == 2);
assertTrue(introductionManager0.getIntroductionMessages(contactId2)
.size() == 2);
assertTrue(introductionManager1.getIntroductionMessages(contactId0)
.size() == 2);
assertTrue(introductionManager2.getIntroductionMessages(contactId0)
.size() == 2);
assertEquals(2,
introductionManager0.getIntroductionMessages(contactId1)
.size());
assertEquals(2,
introductionManager0.getIntroductionMessages(contactId2)
.size());
assertEquals(2,
introductionManager1.getIntroductionMessages(contactId0)
.size());
assertEquals(2,
introductionManager2.getIntroductionMessages(contactId0)
.size());
}
private class IntroduceeListener implements EventListener {

View File

@@ -163,6 +163,7 @@
<string name="introduction_response_declined_sent">You declined the introduction to %1$s.</string>
<string name="introduction_response_accepted_received">%1$s accepted to be introduced to %2$s.</string>
<string name="introduction_response_declined_received">%1$s declined to be introduced to %2$s.</string>
<string name="introduction_response_declined_received_by_introducee">%1$s has informed us that %2$s has declined the introduction.</string>
<string name="introduction_success_title">Introduced contact was added</string>
<string name="introduction_success_text">You have been introduced to %1$s.</string>

View File

@@ -77,9 +77,15 @@ public abstract class ConversationItem {
R.string.introduction_response_accepted_received,
contactName, ir.getName());
} else {
text = ctx.getString(
R.string.introduction_response_declined_received,
contactName, ir.getName());
if (ir.isIntroducer()) {
text = ctx.getString(
R.string.introduction_response_declined_received,
contactName, ir.getName());
} else {
text = ctx.getString(
R.string.introduction_response_declined_received_by_introducee,
contactName, ir.getName());
}
}
return new ConversationNoticeInItem(ir.getMessageId(), text,
ir.getTime(), ir.isRead());

View File

@@ -12,16 +12,27 @@ public interface ProtocolEngine<A, S, M> {
StateUpdate<S, M> onMessageDelivered(S localState, M delivered);
class StateUpdate<S, M> {
public final boolean deleteMessages;
public final boolean deleteMessage;
public final boolean deleteState;
public final S localState;
public final List<M> toSend;
public final List<Event> toBroadcast;
public StateUpdate(boolean deleteMessages, boolean deleteState,
/**
* This class represents an update of the local protocol state.
* It only shows how the state should be updated,
* but does not carry out the updates on its own.
*
* @param deleteMessage whether to delete the message that triggered the state update. This will be ignored for {@link ProtocolEngine#onLocalAction}.
* @param deleteState whether to delete the localState {@link S}
* @param localState the new local state
* @param toSend a list of messages to be sent as part of the state update
* @param toBroadcast a list of events to broadcast as result of the state update
*/
public StateUpdate(boolean deleteMessage, boolean deleteState,
S localState, List<M> toSend, List<Event> toBroadcast) {
this.deleteMessages = deleteMessages;
this.deleteMessage = deleteMessage;
this.deleteState = deleteState;
this.localState = localState;
this.toSend = toSend;

View File

@@ -2,19 +2,24 @@ package org.briarproject.api.introduction;
import org.briarproject.api.sync.MessageId;
import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
abstract public class IntroductionMessage {
private final SessionId sessionId;
private final MessageId messageId;
private final int role;
private final long time;
private final boolean local, sent, seen, read;
public IntroductionMessage(SessionId sessionId, MessageId messageId,
long time, boolean local, boolean sent, boolean seen,
int role, long time, boolean local, boolean sent, boolean seen,
boolean read) {
this.sessionId = sessionId;
this.messageId = messageId;
this.role = role;
this.time = time;
this.local = local;
this.sent = sent;
@@ -50,5 +55,13 @@ abstract public class IntroductionMessage {
return read;
}
public boolean isIntroducer() {
return role == ROLE_INTRODUCER;
}
public boolean isIntroducee() {
return role == ROLE_INTRODUCEE;
}
}

View File

@@ -9,12 +9,13 @@ public class IntroductionRequest extends IntroductionResponse {
private final boolean answered, exists, introducesOtherIdentity;
public IntroductionRequest(SessionId sessionId, MessageId messageId,
long time, boolean local, boolean sent, boolean seen, boolean read,
AuthorId authorId, String name, boolean accepted, String message,
boolean answered, boolean exists, boolean introducesOtherIdentity) {
int role, long time, boolean local, boolean sent, boolean seen,
boolean read, AuthorId authorId, String name, boolean accepted,
String message, boolean answered, boolean exists,
boolean introducesOtherIdentity) {
super(sessionId, messageId, time, local, sent, seen, read, authorId,
name, accepted);
super(sessionId, messageId, role, time, local, sent, seen, read,
authorId, name, accepted);
this.message = message;
this.answered = answered;

View File

@@ -10,10 +10,11 @@ public class IntroductionResponse extends IntroductionMessage {
private final boolean accepted;
public IntroductionResponse(SessionId sessionId, MessageId messageId,
long time, boolean local, boolean sent, boolean seen, boolean read,
AuthorId remoteAuthorId, String name, boolean accepted) {
int role, long time, boolean local, boolean sent, boolean seen,
boolean read, AuthorId remoteAuthorId, String name,
boolean accepted) {
super(sessionId, messageId, time, local, sent, seen, read);
super(sessionId, messageId, role, time, local, sent, seen, read);
this.remoteAuthorId = remoteAuthorId;
this.name = name;
@@ -28,4 +29,7 @@ public class IntroductionResponse extends IntroductionMessage {
return accepted;
}
public AuthorId getRemoteAuthorId() {
return remoteAuthorId;
}
}

View File

@@ -51,6 +51,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.OUR_TIME;
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY;
import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID;
import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_IS_US;
import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
import static org.briarproject.api.introduction.IntroductionConstants.STATE;
import static org.briarproject.api.introduction.IntroductionConstants.TASK;
@@ -195,14 +196,11 @@ public class IntroduceeEngine
messages = Collections.emptyList();
events = Collections.emptyList();
}
// we are done (probably declined response) and ignore this message
// we are done (probably declined response), ignore & delete message
else if (currentState == FINISHED) {
if(action == REMOTE_DECLINE || action == REMOTE_ACCEPT) {
// record response data,
// so we later know which response was ours
addResponseData(localState, msg);
}
return noUpdate(localState);
return new StateUpdate<BdfDictionary, BdfDictionary>(true,
false, localState, new ArrayList<BdfDictionary>(0),
new ArrayList<Event>(0));
}
// this should not happen
else {
@@ -341,8 +339,8 @@ public class IntroduceeEngine
localState.getBoolean(REMOTE_AUTHOR_IS_US);
IntroductionRequest ir = new IntroductionRequest(sessionId, messageId,
time, false, false, false, false, authorId, name, false,
message, false, exists, introducesOtherIdentity);
ROLE_INTRODUCEE, time, false, false, false, false, authorId,
name, false, message, false, exists, introducesOtherIdentity);
return new IntroductionRequestReceivedEvent(contactId, ir);
}

View File

@@ -1,6 +1,5 @@
package org.briarproject.introduction;
import org.briarproject.api.Bytes;
import org.briarproject.api.FormatException;
import org.briarproject.api.TransportId;
@@ -39,6 +38,7 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST;
import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
@@ -51,6 +51,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_K
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
import static org.briarproject.api.introduction.IntroductionConstants.INTRODUCER;
import static org.briarproject.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID;
import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_ID;
import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
import static org.briarproject.api.introduction.IntroductionConstants.NAME;
import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
@@ -170,7 +171,7 @@ class IntroduceeManager {
BdfDictionary message) throws DbException, FormatException {
IntroduceeEngine engine = new IntroduceeEngine();
processStateUpdate(txn, engine.onMessageReceived(state, message));
processStateUpdate(txn, message, engine.onMessageReceived(state, message));
}
public void acceptIntroduction(Transaction txn, BdfDictionary state,
@@ -200,7 +201,7 @@ class IntroduceeManager {
// start engine and process its state update
IntroduceeEngine engine = new IntroduceeEngine();
processStateUpdate(txn, engine.onLocalAction(state, localAction));
processStateUpdate(txn, null, engine.onLocalAction(state, localAction));
}
public void declineIntroduction(Transaction txn, BdfDictionary state,
@@ -217,11 +218,11 @@ class IntroduceeManager {
// start engine and process its state update
IntroduceeEngine engine = new IntroduceeEngine();
processStateUpdate(txn,
processStateUpdate(txn, null,
engine.onLocalAction(state, localAction));
}
private void processStateUpdate(Transaction txn,
private void processStateUpdate(Transaction txn, BdfDictionary msg,
IntroduceeEngine.StateUpdate<BdfDictionary, BdfDictionary>
result) throws DbException, FormatException {
@@ -242,6 +243,16 @@ class IntroduceeManager {
for (Event event : result.toBroadcast) {
txn.attach(event);
}
// delete message
if (result.deleteMessage && msg != null) {
MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID));
if (LOG.isLoggable(INFO)) {
LOG.info("Deleting message with id " + messageId.hashCode());
}
db.deleteMessage(txn, messageId);
db.deleteMessageMetadata(txn, messageId);
}
}
private void performTasks(Transaction txn, BdfDictionary localState)
@@ -253,8 +264,6 @@ class IntroduceeManager {
long task = localState.getLong(TASK);
localState.put(TASK, BdfDictionary.NULL_VALUE);
if (task == TASK_ADD_CONTACT) {
if (localState.getBoolean(EXISTS)) {
// we have this contact already, so do not perform actions
@@ -374,7 +383,7 @@ class IntroduceeManager {
BdfDictionary localAction = new BdfDictionary();
localAction.put(TYPE, TYPE_ABORT);
try {
processStateUpdate(txn,
processStateUpdate(txn, null,
engine.onLocalAction(state, localAction));
} catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);

View File

@@ -55,6 +55,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY2;
import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_1;
import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_2;
import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
import static org.briarproject.api.introduction.IntroductionConstants.STATE;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
@@ -302,8 +303,9 @@ public class IntroducerEngine
boolean accept = msg.getBoolean(ACCEPT);
IntroductionResponse ir =
new IntroductionResponse(sessionId, messageId, time, false,
false, false, false, authorId, name, accept);
new IntroductionResponse(sessionId, messageId, ROLE_INTRODUCER,
time, false, false, false, false, authorId, name,
accept);
return new IntroductionResponseReceivedEvent(contactId, ir);
}

View File

@@ -41,6 +41,7 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static org.briarproject.api.introduction.IntroduceeProtocolState.FINISHED;
import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
import static org.briarproject.api.introduction.IntroductionConstants.ANSWERED;
import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_1;
@@ -331,6 +332,7 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
BdfDictionary state =
getSessionState(txn, g, sessionId.getBytes());
int role = state.getLong(ROLE).intValue();
boolean local;
long time = msg.getLong(MESSAGE_TIME);
boolean accepted = msg.getBoolean(ACCEPT, false);
@@ -338,7 +340,7 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
AuthorId authorId;
String name;
if (type == TYPE_RESPONSE) {
if (state.getLong(ROLE) == ROLE_INTRODUCER) {
if (role == ROLE_INTRODUCER) {
if (!concernsThisContact(contactId, messageId, state)) {
// this response is not from contactId
continue;
@@ -350,22 +352,30 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
} else {
if (Arrays.equals(state.getRaw(NOT_OUR_RESPONSE),
messageId.getBytes())) {
// this response is not ours, don't include it
continue;
// this response is not ours,
// check if it was a decline
if (!accepted) {
local = false;
} else {
// don't include positive responses
continue;
}
} else {
local = true;
}
local = true;
authorId = new AuthorId(
state.getRaw(REMOTE_AUTHOR_ID));
name = state.getString(NAME);
}
IntroductionResponse ir = new IntroductionResponse(
sessionId, messageId, time, local, s.isSent(),
s.isSeen(), read, authorId, name, accepted);
sessionId, messageId, role, time, local,
s.isSent(), s.isSeen(), read, authorId, name,
accepted);
list.add(ir);
} else if (type == TYPE_REQUEST) {
String message;
boolean answered, exists, introducesOtherIdentity;
if (state.getLong(ROLE) == ROLE_INTRODUCER) {
if (role == ROLE_INTRODUCER) {
local = true;
authorId =
getAuthorIdForIntroducer(contactId, state);
@@ -380,15 +390,17 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
state.getRaw(REMOTE_AUTHOR_ID));
name = state.getString(NAME);
message = state.getOptionalString(MSG);
answered = state.getBoolean(ANSWERED);
boolean finished = state.getLong(STATE) ==
FINISHED.getValue();
answered = finished || state.getBoolean(ANSWERED);
exists = state.getBoolean(EXISTS);
introducesOtherIdentity =
state.getBoolean(REMOTE_AUTHOR_IS_US);
}
IntroductionRequest ir = new IntroductionRequest(
sessionId, messageId, time, local, s.isSent(),
s.isSeen(), read, authorId, name, accepted,
message, answered, exists,
sessionId, messageId, role, time, local,
s.isSent(), s.isSeen(), read, authorId, name,
accepted, message, answered, exists,
introducesOtherIdentity);
list.add(ir);
}