Implement conversation data persistence

This commit is contained in:
str4d
2016-06-21 13:51:31 +00:00
parent f750280845
commit db52b2c29f
8 changed files with 664 additions and 299 deletions

View File

@@ -36,6 +36,7 @@
<string name="contact_list_button">Contacts</string> <string name="contact_list_button">Contacts</string>
<string name="delete_contact">Delete contact</string> <string name="delete_contact">Delete contact</string>
<string name="contact_deleted_toast">Contact deleted</string> <string name="contact_deleted_toast">Contact deleted</string>
<string name="contact_deletion_failed_toast">Failed to delete contact</string>
<string name="forums_button">Forums</string> <string name="forums_button">Forums</string>
<string name="settings_button">Settings</string> <string name="settings_button">Settings</string>
<string name="sign_out_button">Sign Out</string> <string name="sign_out_button">Sign Out</string>

View File

@@ -4,6 +4,8 @@ import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import org.briarproject.android.contact.ConversationController;
import org.briarproject.android.contact.ConversationControllerImpl;
import org.briarproject.android.controller.BriarController; import org.briarproject.android.controller.BriarController;
import org.briarproject.android.controller.BriarControllerImpl; import org.briarproject.android.controller.BriarControllerImpl;
import org.briarproject.android.controller.ConfigController; import org.briarproject.android.controller.ConfigController;
@@ -91,6 +93,14 @@ public class ActivityModule {
return dbController; return dbController;
} }
@ActivityScope
@Provides
protected ConversationController provideConversationController(
ConversationControllerImpl conversationController) {
activity.addLifecycleController(conversationController);
return conversationController;
}
@ActivityScope @ActivityScope
@Provides @Provides
protected ForumController provideForumController( protected ForumController provideForumController(

View File

@@ -5,6 +5,7 @@ import org.briarproject.CoreModule;
import org.briarproject.android.api.AndroidExecutor; import org.briarproject.android.api.AndroidExecutor;
import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.api.ReferenceManager; import org.briarproject.android.api.ReferenceManager;
import org.briarproject.android.contact.ConversationPersistentData;
import org.briarproject.android.forum.ForumPersistentData; import org.briarproject.android.forum.ForumPersistentData;
import org.briarproject.android.report.BriarReportSender; import org.briarproject.android.report.BriarReportSender;
import org.briarproject.api.contact.ContactExchangeTask; import org.briarproject.api.contact.ContactExchangeTask;
@@ -113,6 +114,8 @@ public interface AndroidComponent extends CoreEagerSingletons {
AndroidExecutor androidExecutor(); AndroidExecutor androidExecutor();
ConversationPersistentData conversationPersistentData();
ForumPersistentData forumPersistentData(); ForumPersistentData forumPersistentData();
@IoExecutor @IoExecutor

View File

@@ -4,6 +4,7 @@ import android.app.Application;
import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.api.ReferenceManager; import org.briarproject.android.api.ReferenceManager;
import org.briarproject.android.contact.ConversationPersistentData;
import org.briarproject.android.forum.ForumPersistentData; import org.briarproject.android.forum.ForumPersistentData;
import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.PublicKey; import org.briarproject.api.crypto.PublicKey;
@@ -138,6 +139,12 @@ public class AppModule {
return notificationManager; return notificationManager;
} }
@Provides
@Singleton
ConversationPersistentData provideConversationPersistence() {
return new ConversationPersistentData();
}
@Provides @Provides
@Singleton @Singleton
ForumPersistentData provideForumPersistence() { ForumPersistentData provideForumPersistence() {

View File

@@ -27,30 +27,12 @@ import org.briarproject.R;
import org.briarproject.android.ActivityComponent; import org.briarproject.android.ActivityComponent;
import org.briarproject.android.BriarActivity; import org.briarproject.android.BriarActivity;
import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.android.introduction.IntroductionActivity; import org.briarproject.android.introduction.IntroductionActivity;
import org.briarproject.android.util.BriarRecyclerView; import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.FormatException;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.conversation.ConversationItem; import org.briarproject.api.conversation.ConversationItem;
import org.briarproject.api.conversation.ConversationItem.IncomingItem; import org.briarproject.api.conversation.ConversationItem.IncomingItem;
import org.briarproject.api.conversation.ConversationManager;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.event.ContactConnectedEvent;
import org.briarproject.api.event.ContactDisconnectedEvent;
import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.ConversationItemReceivedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessagesAckedEvent;
import org.briarproject.api.event.MessagesSentEvent;
import org.briarproject.api.messaging.PrivateMessage;
import org.briarproject.api.messaging.PrivateMessageFactory;
import org.briarproject.api.plugins.ConnectionRegistry;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
@@ -61,7 +43,6 @@ import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
@@ -72,10 +53,9 @@ import im.delight.android.identicons.IdenticonDrawable;
import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation; import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
import static android.widget.Toast.LENGTH_SHORT; import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
public class ConversationActivity extends BriarActivity public class ConversationActivity extends BriarActivity
implements EventListener, OnClickListener, implements ConversationController.ConversationListener, OnClickListener,
ConversationAdapter.ConversationHandler, ConversationAdapter.ConversationHandler,
ConversationAdapter.MessageUpdatedHandler { ConversationAdapter.MessageUpdatedHandler {
@@ -85,11 +65,6 @@ public class ConversationActivity extends BriarActivity
@Inject @Inject
AndroidNotificationManager notificationManager; AndroidNotificationManager notificationManager;
@Inject
ConnectionRegistry connectionRegistry;
@Inject
@CryptoExecutor
protected Executor cryptoExecutor;
private ConversationAdapter adapter; private ConversationAdapter adapter;
private CircleImageView toolbarAvatar; private CircleImageView toolbarAvatar;
@@ -101,19 +76,9 @@ public class ConversationActivity extends BriarActivity
// Fields that are accessed from background threads must be volatile // Fields that are accessed from background threads must be volatile
@Inject @Inject
protected volatile ContactManager contactManager; protected volatile ConversationController conversationController;
@Inject
protected volatile ConversationManager conversationManager;
@Inject
protected volatile EventBus eventBus;
@Inject
volatile PrivateMessageFactory privateMessageFactory;
private volatile GroupId groupId = null; private volatile GroupId groupId = null;
private volatile ContactId contactId = null;
private volatile String contactName = null;
private volatile byte[] contactIdenticonKey = null;
private volatile boolean connected = false;
@Override @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
@@ -161,6 +126,22 @@ public class ConversationActivity extends BriarActivity
sendButton.setEnabled(false); sendButton.setEnabled(false);
sendButton.setOnClickListener(this); sendButton.setOnClickListener(this);
} }
conversationController
.loadConversation(groupId, new UiResultHandler<Boolean>(this) {
@Override
public void onResultUi(Boolean result) {
if (result) {
displayContactDetails();
// Load the messages here to make sure we have a
// contactId
loadMessages();
} else {
// TODO Maybe an error dialog ?
finish();
}
}
});
} }
@Override @Override
@@ -183,16 +164,13 @@ public class ConversationActivity extends BriarActivity
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
eventBus.addListener(this);
notificationManager.blockNotification(groupId); notificationManager.blockNotification(groupId);
notificationManager.clearPrivateMessageNotification(groupId); notificationManager.clearPrivateMessageNotification(groupId);
loadData();
} }
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
eventBus.removeListener(this);
notificationManager.unblockNotification(groupId); notificationManager.unblockNotification(groupId);
if (isFinishing()) markMessagesRead(); if (isFinishing()) markMessagesRead();
} }
@@ -203,8 +181,16 @@ public class ConversationActivity extends BriarActivity
MenuInflater inflater = getMenuInflater(); MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.conversation_actions, menu); inflater.inflate(R.menu.conversation_actions, menu);
hideIntroductionActionWhenOneContact( final MenuItem introduction = menu.findItem(R.id.action_introduction);
menu.findItem(R.id.action_introduction)); conversationController.shouldHideIntroductionAction(
new UiResultHandler<Boolean>(this) {
@Override
public void onResultUi(Boolean result) {
if (result) {
introduction.setVisible(false);
}
}
});
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@@ -217,6 +203,7 @@ public class ConversationActivity extends BriarActivity
onBackPressed(); onBackPressed();
return true; return true;
case R.id.action_introduction: case R.id.action_introduction:
ContactId contactId = conversationController.getContactId();
if (contactId == null) return false; if (contactId == null) return false;
Intent intent = new Intent(this, IntroductionActivity.class); Intent intent = new Intent(this, IntroductionActivity.class);
intent.putExtra(IntroductionActivity.CONTACT_ID, intent.putExtra(IntroductionActivity.CONTACT_ID,
@@ -242,46 +229,18 @@ public class ConversationActivity extends BriarActivity
finish(); finish();
} }
private void loadData() { /**
runOnDbThread(new Runnable() { * This should only be called after the conversation has been loaded.
@Override */
public void run() {
try {
long now = System.currentTimeMillis();
if (contactId == null)
contactId = conversationManager.getContactId(groupId);
if (contactName == null || contactIdenticonKey == null) {
Contact contact = contactManager.getContact(contactId);
contactName = contact.getAuthor().getName();
contactIdenticonKey =
contact.getAuthor().getId().getBytes();
}
connected = connectionRegistry.isConnected(contactId);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading contact took " + duration + " ms");
displayContactDetails();
// Load the messages here to make sure we have a contactId
loadMessages();
} catch (NoSuchContactException e) {
finishOnUiThread();
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayContactDetails() { private void displayContactDetails() {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
toolbarAvatar.setImageDrawable( toolbarAvatar.setImageDrawable(new IdenticonDrawable(
new IdenticonDrawable(contactIdenticonKey)); conversationController.getContactIdenticonKey()));
toolbarTitle.setText(contactName); toolbarTitle.setText(conversationController.getContactName());
if (connected) { if (conversationController.isConnected()) {
toolbarStatus.setImageDrawable(ContextCompat toolbarStatus.setImageDrawable(ContextCompat
.getDrawable(ConversationActivity.this, .getDrawable(ConversationActivity.this,
R.drawable.contact_online)); R.drawable.contact_online));
@@ -294,48 +253,30 @@ public class ConversationActivity extends BriarActivity
toolbarStatus toolbarStatus
.setContentDescription(getString(R.string.offline)); .setContentDescription(getString(R.string.offline));
} }
adapter.setContactName(contactName); adapter.setContactName(conversationController.getContactName());
} }
}); });
} }
private void loadMessages() { private void loadMessages() {
runOnDbThread(new Runnable() { conversationController.loadMessages(new UiResultHandler<Boolean>(this) {
@Override @Override
public void run() { public void onResultUi(Boolean result) {
try { if (result) {
long now = System.currentTimeMillis();
if (contactId == null)
contactId = conversationManager.getContactId(groupId);
List<ConversationItem> items = List<ConversationItem> items =
conversationManager.getMessages(contactId); conversationController.getConversationItems();
long duration = System.currentTimeMillis() - now; sendButton.setEnabled(true);
if (LOG.isLoggable(INFO)) if (items.isEmpty()) {
LOG.info("Loading headers took " + duration + " ms"); // we have no messages,
displayMessages(items); // so let the list know to hide progress bar
} catch (NoSuchContactException e) { list.showData();
finishOnUiThread(); } else {
} catch (DbException e) { adapter.addAll(items);
if (LOG.isLoggable(WARNING)) // Scroll to the bottom
LOG.log(WARNING, e.toString(), e); list.scrollToPosition(adapter.getItemCount() - 1);
} }
}
});
}
private void displayMessages(final List<ConversationItem> items) {
runOnUiThread(new Runnable() {
@Override
public void run() {
sendButton.setEnabled(true);
if (items.isEmpty()) {
// we have no messages,
// so let the list know to hide progress bar
list.showData();
} else { } else {
adapter.addAll(items); finish();
// Scroll to the bottom
list.scrollToPosition(adapter.getItemCount() - 1);
} }
} }
}); });
@@ -362,71 +303,14 @@ public class ConversationActivity extends BriarActivity
if (unread.isEmpty()) return; if (unread.isEmpty()) return;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Marking " + unread.size() + " messages read"); LOG.info("Marking " + unread.size() + " messages read");
markMessagesRead(Collections.unmodifiableList(unread)); conversationController.markMessagesRead(
} Collections.unmodifiableList(unread),
new UiResultHandler<Boolean>(this) {
private void markMessagesRead(final Collection<ConversationItem> unread) { @Override
runOnDbThread(new Runnable() { public void onResultUi(Boolean result) {
@Override // TODO something?
public void run() { }
try { });
long now = System.currentTimeMillis();
for (ConversationItem item : unread)
conversationManager.setReadFlag(contactId, item, true);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Marking read took " + duration + " ms");
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
@Override
public void eventOccurred(Event e) {
if (e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e;
if (c.getContactId().equals(contactId)) {
LOG.info("Contact removed");
finishOnUiThread();
}
} else if (e instanceof ConversationItemReceivedEvent) {
ConversationItemReceivedEvent event =
(ConversationItemReceivedEvent) e;
if (event.getContactId().equals(contactId)) {
LOG.info("Message received, adding");
addConversationItem(event.getItem());
markMessageReadIfNew(event.getItem());
}
} else if (e instanceof MessagesSentEvent) {
MessagesSentEvent m = (MessagesSentEvent) e;
if (m.getContactId().equals(contactId)) {
LOG.info("Messages sent");
markMessages(m.getMessageIds(), true, false);
}
} else if (e instanceof MessagesAckedEvent) {
MessagesAckedEvent m = (MessagesAckedEvent) e;
if (m.getContactId().equals(contactId)) {
LOG.info("Messages acked");
markMessages(m.getMessageIds(), true, true);
}
} else if (e instanceof ContactConnectedEvent) {
ContactConnectedEvent c = (ContactConnectedEvent) e;
if (c.getContactId().equals(contactId)) {
LOG.info("Contact connected");
connected = true;
displayContactDetails();
}
} else if (e instanceof ContactDisconnectedEvent) {
ContactDisconnectedEvent c = (ContactDisconnectedEvent) e;
if (c.getContactId().equals(contactId)) {
LOG.info("Contact disconnected");
connected = false;
displayContactDetails();
}
}
} }
private void markMessageReadIfNew(final ConversationItem item) { private void markMessageReadIfNew(final ConversationItem item) {
@@ -438,32 +322,19 @@ public class ConversationActivity extends BriarActivity
// Mark the message read if it's the newest message // Mark the message read if it's the newest message
long lastMsgTime = last.getTime(); long lastMsgTime = last.getTime();
long newMsgTime = item.getTime(); long newMsgTime = item.getTime();
if (newMsgTime > lastMsgTime) markNewMessageRead(item); if (newMsgTime > lastMsgTime)
else loadMessages(); conversationController.markNewMessageRead(item);
} else { } else {
// mark the message as read as well if it is the first one // mark the message as read as well if it is the first one
markNewMessageRead(item); conversationController.markNewMessageRead(item);
} }
loadMessages();
} }
}); });
} }
private void markNewMessageRead(final ConversationItem item) { @Override
runOnDbThread(new Runnable() { public void markMessages(final Collection<MessageId> messageIds,
@Override
public void run() {
try {
conversationManager.setReadFlag(contactId, item, true);
loadMessages();
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void markMessages(final Collection<MessageId> messageIds,
final boolean sent, final boolean seen) { final boolean sent, final boolean seen) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
@@ -483,6 +354,12 @@ public class ConversationActivity extends BriarActivity
}); });
} }
@Override
public void messageReceived(ConversationItem item) {
addConversationItem(item);
markMessageReadIfNew(item);
}
@Override @Override
public void onClick(View view) { public void onClick(View view) {
markMessagesRead(); markMessagesRead();
@@ -501,37 +378,14 @@ public class ConversationActivity extends BriarActivity
} }
private void createMessage(final byte[] body, final long timestamp) { private void createMessage(final byte[] body, final long timestamp) {
cryptoExecutor.execute(new Runnable() { conversationController.createMessage(body, timestamp,
@Override new UiResultHandler<ConversationItem>(this) {
public void run() { @Override
try { public void onResultUi(ConversationItem item) {
storeMessage(privateMessageFactory if (item != null)
.createPrivateMessage(groupId, timestamp, null, addConversationItem(item);
"text/plain", body), body); }
} catch (FormatException e) { });
throw new RuntimeException(e);
}
}
});
}
private void storeMessage(final PrivateMessage m, final byte[] body) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
long now = System.currentTimeMillis();
ConversationItem item = conversationManager.addLocalMessage(m, body);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Storing message took " + duration + " ms");
addConversationItem(item);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
} }
private void askToRemoveContact() { private void askToRemoveContact() {
@@ -553,82 +407,42 @@ public class ConversationActivity extends BriarActivity
} }
private void removeContact() { private void removeContact() {
runOnDbThread(new Runnable() { conversationController
@Override .removeContact(new UiResultHandler<Boolean>(this) {
public void run() { @Override
try { public void onResultUi(Boolean result) {
// make sure contactId is initialised if (result) {
if (contactId == null) String deleted =
contactId = conversationManager.getContactId(groupId); getString(R.string.contact_deleted_toast);
// remove contact with that ID Toast.makeText(ConversationActivity.this, deleted,
contactManager.removeContact(contactId); LENGTH_SHORT)
} catch (DbException e) { .show();
if (LOG.isLoggable(WARNING)) finish();
LOG.log(WARNING, e.toString(), e); } else {
} finally { String failed = getString(
finishAfterContactRemoved(); R.string.contact_deletion_failed_toast);
} Toast.makeText(ConversationActivity.this, failed,
} LENGTH_SHORT).show();
}); }
}
private void finishAfterContactRemoved() {
runOnUiThread(new Runnable() {
@Override
public void run() {
String deleted = getString(R.string.contact_deleted_toast);
Toast.makeText(ConversationActivity.this, deleted, LENGTH_SHORT)
.show();
finish();
}
});
}
private void hideIntroductionActionWhenOneContact(final MenuItem item) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
if (contactManager.getActiveContacts().size() < 2) {
hideIntroductionAction(item);
} }
} catch (DbException e) { });
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void hideIntroductionAction(final MenuItem item) {
runOnUiThread(new Runnable() {
@Override
public void run() {
item.setVisible(false);
}
});
} }
@Override @Override
public void respondToItem(final ConversationItem item, public void respondToItem(ConversationItem item, boolean accept) {
final boolean accept) { long minTimestamp = getMinTimestampForNewMessage();
runOnDbThread(new Runnable() { conversationController.respondToItem(item, accept, minTimestamp,
@Override new UiResultHandler<Boolean>(this) {
public void run() { @Override
long timestamp = System.currentTimeMillis(); public void onResultUi(Boolean result) {
timestamp = Math.max(timestamp, getMinTimestampForNewMessage()); if (result) {
try { loadMessages();
conversationManager } else {
.respondToItem(contactId, item, accept, timestamp); // TODO decide how to make this type-agnostic
loadMessages(); introductionResponseError();
} catch (DbException | FormatException e) { }
// TODO decide how to make this type-agnostic }
introductionResponseError(); });
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
} }
private void introductionResponseError() { private void introductionResponseError() {
@@ -653,4 +467,9 @@ public class ConversationActivity extends BriarActivity
} }
}); });
} }
@Override
public void contactUpdated() {
displayContactDetails();
}
} }

View File

@@ -0,0 +1,53 @@
package org.briarproject.android.contact;
import org.briarproject.android.controller.ActivityLifecycleController;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.conversation.ConversationItem;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import java.util.Collection;
import java.util.List;
public interface ConversationController extends ActivityLifecycleController {
void loadConversation(GroupId groupId,
UiResultHandler<Boolean> resultHandler);
void loadMessages(UiResultHandler<Boolean> resultHandler);
void createMessage(byte[] body, long timestamp,
UiResultHandler<ConversationItem> resultHandler);
ContactId getContactId();
String getContactName();
byte[] getContactIdenticonKey();
List<ConversationItem> getConversationItems();
boolean isConnected();
void markMessagesRead(Collection<ConversationItem> unread,
UiResultHandler<Boolean> resultHandler);
void markNewMessageRead(ConversationItem item);
void removeContact(UiResultHandler<Boolean> resultHandler);
void respondToItem(ConversationItem item, boolean accept, long minTimestamp,
UiResultHandler<Boolean> resultHandler);
void shouldHideIntroductionAction(UiResultHandler<Boolean> resultHandler);
interface ConversationListener {
void contactUpdated();
void markMessages(Collection<MessageId> messageIds, boolean sent,
boolean seen);
void messageReceived(ConversationItem item);
}
}

View File

@@ -0,0 +1,408 @@
package org.briarproject.android.contact;
import android.app.Activity;
import org.briarproject.android.controller.DbControllerImpl;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.api.FormatException;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.conversation.ConversationItem;
import org.briarproject.api.conversation.ConversationManager;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.event.ContactConnectedEvent;
import org.briarproject.api.event.ContactDisconnectedEvent;
import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.ConversationItemReceivedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessagesAckedEvent;
import org.briarproject.api.event.MessagesSentEvent;
import org.briarproject.api.messaging.PrivateMessage;
import org.briarproject.api.messaging.PrivateMessageFactory;
import org.briarproject.api.plugins.ConnectionRegistry;
import org.briarproject.api.sync.GroupId;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
public class ConversationControllerImpl extends DbControllerImpl
implements ConversationController, EventListener {
private static final Logger LOG =
Logger.getLogger(ConversationControllerImpl.class.getName());
@Inject
protected Activity activity;
@Inject
protected ConnectionRegistry connectionRegistry;
@Inject
@CryptoExecutor
protected Executor cryptoExecutor;
// Fields that are accessed from background threads must be volatile
@Inject
protected volatile ContactManager contactManager;
@Inject
protected volatile ConversationManager conversationManager;
@Inject
protected volatile EventBus eventBus;
@Inject
protected volatile PrivateMessageFactory privateMessageFactory;
@Inject
protected ConversationPersistentData data;
private ConversationListener listener;
@Inject
ConversationControllerImpl() {
}
@Override
public void onActivityCreate() {
if (activity instanceof ConversationListener) {
listener = (ConversationListener) activity;
} else {
throw new IllegalStateException(
"An activity that injects the ConversationController " +
"must implement the ConversationListener");
}
}
@Override
public void onActivityResume() {
eventBus.addListener(this);
}
@Override
public void onActivityPause() {
eventBus.removeListener(this);
}
@Override
public void onActivityDestroy() {
if (activity.isFinishing()) {
data.clearAll();
}
}
@Override
public void loadConversation(final GroupId groupId,
final UiResultHandler<Boolean> resultHandler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
if (data.getGroupId() == null ||
!data.getGroupId().equals(groupId)) {
data.setGroupId(groupId);
long now = System.currentTimeMillis();
ContactId contactId =
conversationManager.getContactId(groupId);
data.setContact(contactManager.getContact(contactId));
data.setConnected(
connectionRegistry.isConnected(contactId));
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info(
"Loading contact took " + duration + " ms");
}
resultHandler.onResult(true);
} catch (NoSuchContactException e) {
resultHandler.onResult(false);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(false);
}
}
});
}
@Override
public void loadMessages(final UiResultHandler<Boolean> resultHandler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
if (getContactId() != null) {
long now = System.currentTimeMillis();
data.addConversationItems(
conversationManager
.getMessages(getContactId()));
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info(
"Loading headers took " + duration + " ms");
}
resultHandler.onResult(true);
} catch (NoSuchContactException e) {
resultHandler.onResult(false);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(false);
}
}
});
}
@Override
public void createMessage(final byte[] body, final long timestamp,
final UiResultHandler<ConversationItem> resultHandler) {
cryptoExecutor.execute(new Runnable() {
@Override
public void run() {
try {
PrivateMessage m = privateMessageFactory
.createPrivateMessage(data.getGroupId(), timestamp,
null, "text/plain", body);
storeMessage(m, body, resultHandler);
} catch (FormatException e) {
// TODO why was this being thrown?
//throw new RuntimeException(e);
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(null);
}
}
});
}
private void storeMessage(final PrivateMessage m, final byte[] body,
final UiResultHandler<ConversationItem> resultHandler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
long now = System.currentTimeMillis();
ConversationItem item =
conversationManager.addLocalMessage(m, body);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Storing message took " + duration + " ms");
resultHandler.onResult(item);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(null);
}
}
});
}
@Override
public ContactId getContactId() {
return data.getContact() == null ? null : data.getContact().getId();
}
@Override
public String getContactName() {
return data.getContact() == null ? null :
data.getContact().getAuthor().getName();
}
@Override
public byte[] getContactIdenticonKey() {
return data.getContact() == null ? null :
data.getContact().getAuthor().getId().getBytes();
}
@Override
public List<ConversationItem> getConversationItems() {
return data.getConversationItems();
}
@Override
public boolean isConnected() {
return data.isConnected();
}
@Override
public void markMessagesRead(final Collection<ConversationItem> unread,
final UiResultHandler<Boolean> resultHandler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
if (getContactId() != null) {
long now = System.currentTimeMillis();
for (ConversationItem item : unread)
conversationManager
.setReadFlag(getContactId(), item, true);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Marking read took " + duration + " ms");
}
resultHandler.onResult(true);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(false);
}
}
});
}
@Override
public void markNewMessageRead(final ConversationItem item) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
if (getContactId() != null) {
conversationManager
.setReadFlag(getContactId(), item, true);
}
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
@Override
public void removeContact(final UiResultHandler<Boolean> resultHandler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
if (getContactId() != null) {
contactManager.removeContact(getContactId());
}
resultHandler.onResult(true);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(false);
}
}
});
}
@Override
public void respondToItem(final ConversationItem item, final boolean accept,
final long minTimestamp,
final UiResultHandler<Boolean> resultHandler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, minTimestamp);
try {
conversationManager
.respondToItem(getContactId(), item, accept,
timestamp);
resultHandler.onResult(true);
} catch (DbException | FormatException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(false);
}
}
});
}
@Override
public void shouldHideIntroductionAction(
final UiResultHandler<Boolean> resultHandler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
resultHandler.onResult(
contactManager.getActiveContacts().size() < 2);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(false);
}
}
});
}
@Override
public void eventOccurred(Event e) {
if (e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e;
if (c.getContactId().equals(getContactId())) {
LOG.info("Contact removed");
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.finish();
}
});
}
} else if (e instanceof ConversationItemReceivedEvent) {
final ConversationItemReceivedEvent event =
(ConversationItemReceivedEvent) e;
if (event.getContactId().equals(getContactId())) {
LOG.info("Message received, adding");
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.messageReceived(event.getItem());
}
});
}
} else if (e instanceof MessagesSentEvent) {
final MessagesSentEvent m = (MessagesSentEvent) e;
if (m.getContactId().equals(getContactId())) {
LOG.info("Messages sent");
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.markMessages(m.getMessageIds(), true, false);
}
});
}
} else if (e instanceof MessagesAckedEvent) {
final MessagesAckedEvent m = (MessagesAckedEvent) e;
if (m.getContactId().equals(getContactId())) {
LOG.info("Messages acked");
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.markMessages(m.getMessageIds(), true, true);
}
});
}
} else if (e instanceof ContactConnectedEvent) {
ContactConnectedEvent c = (ContactConnectedEvent) e;
if (c.getContactId().equals(getContactId())) {
LOG.info("Contact connected");
data.setConnected(true);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.contactUpdated();
}
});
}
} else if (e instanceof ContactDisconnectedEvent) {
ContactDisconnectedEvent c = (ContactDisconnectedEvent) e;
if (c.getContactId().equals(getContactId())) {
LOG.info("Contact disconnected");
data.setConnected(false);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.contactUpdated();
}
});
}
}
}
}

View File

@@ -0,0 +1,64 @@
package org.briarproject.android.contact;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.conversation.ConversationItem;
import org.briarproject.api.sync.GroupId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
/**
* This class is a singleton that defines the data that should persist, i.e.
* still be present in memory after activity restarts. This class is not thread
* safe.
*/
public class ConversationPersistentData {
private volatile GroupId groupId;
private volatile Contact contact;
private volatile boolean connected;
private volatile List<ConversationItem> items = new ArrayList<>();
public void clearAll() {
groupId = null;
contact = null;
connected = false;
items.clear();
}
public GroupId getGroupId() {
return groupId;
}
public void setGroupId(GroupId groupId) {
this.groupId = groupId;
}
public Contact getContact() {
return contact;
}
public void setContact(Contact contact) {
this.contact = contact;
}
public boolean isConnected() {
return connected;
}
public void setConnected(boolean connected) {
this.connected = connected;
}
public void addConversationItems(Collection<ConversationItem> items) {
this.items.addAll(items);
}
public List<ConversationItem> getConversationItems() {
return Collections.unmodifiableList(items);
}
}