From db52b2c29fcaf7dfb63a579fab51e29cdb16697b Mon Sep 17 00:00:00 2001 From: str4d Date: Tue, 21 Jun 2016 13:51:31 +0000 Subject: [PATCH] Implement conversation data persistence --- briar-android/res/values/strings.xml | 1 + .../briarproject/android/ActivityModule.java | 10 + .../android/AndroidComponent.java | 3 + .../org/briarproject/android/AppModule.java | 7 + .../android/contact/ConversationActivity.java | 417 +++++------------- .../contact/ConversationController.java | 53 +++ .../contact/ConversationControllerImpl.java | 408 +++++++++++++++++ .../contact/ConversationPersistentData.java | 64 +++ 8 files changed, 664 insertions(+), 299 deletions(-) create mode 100644 briar-android/src/org/briarproject/android/contact/ConversationController.java create mode 100644 briar-android/src/org/briarproject/android/contact/ConversationControllerImpl.java create mode 100644 briar-android/src/org/briarproject/android/contact/ConversationPersistentData.java diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index d44bc389a..aebe567c6 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -36,6 +36,7 @@ Contacts Delete contact Contact deleted + Failed to delete contact Forums Settings Sign Out diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java index ea984bca0..502da98fb 100644 --- a/briar-android/src/org/briarproject/android/ActivityModule.java +++ b/briar-android/src/org/briarproject/android/ActivityModule.java @@ -4,6 +4,8 @@ import android.app.Activity; import android.content.Context; 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.BriarControllerImpl; import org.briarproject.android.controller.ConfigController; @@ -91,6 +93,14 @@ public class ActivityModule { return dbController; } + @ActivityScope + @Provides + protected ConversationController provideConversationController( + ConversationControllerImpl conversationController) { + activity.addLifecycleController(conversationController); + return conversationController; + } + @ActivityScope @Provides protected ForumController provideForumController( diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java index d5b9a8393..25054d1ab 100644 --- a/briar-android/src/org/briarproject/android/AndroidComponent.java +++ b/briar-android/src/org/briarproject/android/AndroidComponent.java @@ -5,6 +5,7 @@ import org.briarproject.CoreModule; import org.briarproject.android.api.AndroidExecutor; import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.api.ReferenceManager; +import org.briarproject.android.contact.ConversationPersistentData; import org.briarproject.android.forum.ForumPersistentData; import org.briarproject.android.report.BriarReportSender; import org.briarproject.api.contact.ContactExchangeTask; @@ -113,6 +114,8 @@ public interface AndroidComponent extends CoreEagerSingletons { AndroidExecutor androidExecutor(); + ConversationPersistentData conversationPersistentData(); + ForumPersistentData forumPersistentData(); @IoExecutor diff --git a/briar-android/src/org/briarproject/android/AppModule.java b/briar-android/src/org/briarproject/android/AppModule.java index 36933a118..f79a5b90d 100644 --- a/briar-android/src/org/briarproject/android/AppModule.java +++ b/briar-android/src/org/briarproject/android/AppModule.java @@ -4,6 +4,7 @@ import android.app.Application; import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.api.ReferenceManager; +import org.briarproject.android.contact.ConversationPersistentData; import org.briarproject.android.forum.ForumPersistentData; import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.PublicKey; @@ -138,6 +139,12 @@ public class AppModule { return notificationManager; } + @Provides + @Singleton + ConversationPersistentData provideConversationPersistence() { + return new ConversationPersistentData(); + } + @Provides @Singleton ForumPersistentData provideForumPersistence() { diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java index dd630c368..c0eacc944 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java @@ -27,30 +27,12 @@ import org.briarproject.R; import org.briarproject.android.ActivityComponent; import org.briarproject.android.BriarActivity; import org.briarproject.android.api.AndroidNotificationManager; +import org.briarproject.android.controller.handler.UiResultHandler; import org.briarproject.android.introduction.IntroductionActivity; 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.ContactManager; import org.briarproject.api.conversation.ConversationItem; 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.MessageId; import org.briarproject.util.StringUtils; @@ -61,7 +43,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.Executor; import java.util.logging.Logger; 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.widget.Toast.LENGTH_SHORT; import static java.util.logging.Level.INFO; -import static java.util.logging.Level.WARNING; public class ConversationActivity extends BriarActivity - implements EventListener, OnClickListener, + implements ConversationController.ConversationListener, OnClickListener, ConversationAdapter.ConversationHandler, ConversationAdapter.MessageUpdatedHandler { @@ -85,11 +65,6 @@ public class ConversationActivity extends BriarActivity @Inject AndroidNotificationManager notificationManager; - @Inject - ConnectionRegistry connectionRegistry; - @Inject - @CryptoExecutor - protected Executor cryptoExecutor; private ConversationAdapter adapter; private CircleImageView toolbarAvatar; @@ -101,19 +76,9 @@ public class ConversationActivity extends BriarActivity // 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 - volatile PrivateMessageFactory privateMessageFactory; + protected volatile ConversationController conversationController; 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 public void onCreate(Bundle state) { @@ -161,6 +126,22 @@ public class ConversationActivity extends BriarActivity sendButton.setEnabled(false); sendButton.setOnClickListener(this); } + + conversationController + .loadConversation(groupId, new UiResultHandler(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 @@ -183,16 +164,13 @@ public class ConversationActivity extends BriarActivity @Override public void onResume() { super.onResume(); - eventBus.addListener(this); notificationManager.blockNotification(groupId); notificationManager.clearPrivateMessageNotification(groupId); - loadData(); } @Override public void onPause() { super.onPause(); - eventBus.removeListener(this); notificationManager.unblockNotification(groupId); if (isFinishing()) markMessagesRead(); } @@ -203,8 +181,16 @@ public class ConversationActivity extends BriarActivity MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.conversation_actions, menu); - hideIntroductionActionWhenOneContact( - menu.findItem(R.id.action_introduction)); + final MenuItem introduction = menu.findItem(R.id.action_introduction); + conversationController.shouldHideIntroductionAction( + new UiResultHandler(this) { + @Override + public void onResultUi(Boolean result) { + if (result) { + introduction.setVisible(false); + } + } + }); return super.onCreateOptionsMenu(menu); } @@ -217,6 +203,7 @@ public class ConversationActivity extends BriarActivity onBackPressed(); return true; case R.id.action_introduction: + ContactId contactId = conversationController.getContactId(); if (contactId == null) return false; Intent intent = new Intent(this, IntroductionActivity.class); intent.putExtra(IntroductionActivity.CONTACT_ID, @@ -242,46 +229,18 @@ public class ConversationActivity extends BriarActivity finish(); } - private void loadData() { - runOnDbThread(new Runnable() { - @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); - } - } - }); - } - + /** + * This should only be called after the conversation has been loaded. + */ private void displayContactDetails() { runOnUiThread(new Runnable() { @Override public void run() { - toolbarAvatar.setImageDrawable( - new IdenticonDrawable(contactIdenticonKey)); - toolbarTitle.setText(contactName); + toolbarAvatar.setImageDrawable(new IdenticonDrawable( + conversationController.getContactIdenticonKey())); + toolbarTitle.setText(conversationController.getContactName()); - if (connected) { + if (conversationController.isConnected()) { toolbarStatus.setImageDrawable(ContextCompat .getDrawable(ConversationActivity.this, R.drawable.contact_online)); @@ -294,48 +253,30 @@ public class ConversationActivity extends BriarActivity toolbarStatus .setContentDescription(getString(R.string.offline)); } - adapter.setContactName(contactName); + adapter.setContactName(conversationController.getContactName()); } }); } private void loadMessages() { - runOnDbThread(new Runnable() { + conversationController.loadMessages(new UiResultHandler(this) { @Override - public void run() { - try { - long now = System.currentTimeMillis(); - if (contactId == null) - contactId = conversationManager.getContactId(groupId); + public void onResultUi(Boolean result) { + if (result) { List items = - conversationManager.getMessages(contactId); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Loading headers took " + duration + " ms"); - displayMessages(items); - } catch (NoSuchContactException e) { - finishOnUiThread(); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - } - } - }); - } - - private void displayMessages(final List 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(); + conversationController.getConversationItems(); + sendButton.setEnabled(true); + if (items.isEmpty()) { + // we have no messages, + // so let the list know to hide progress bar + list.showData(); + } else { + adapter.addAll(items); + // Scroll to the bottom + list.scrollToPosition(adapter.getItemCount() - 1); + } } else { - adapter.addAll(items); - // Scroll to the bottom - list.scrollToPosition(adapter.getItemCount() - 1); + finish(); } } }); @@ -362,71 +303,14 @@ public class ConversationActivity extends BriarActivity if (unread.isEmpty()) return; if (LOG.isLoggable(INFO)) LOG.info("Marking " + unread.size() + " messages read"); - markMessagesRead(Collections.unmodifiableList(unread)); - } - - private void markMessagesRead(final Collection unread) { - runOnDbThread(new Runnable() { - @Override - 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(); - } - } + conversationController.markMessagesRead( + Collections.unmodifiableList(unread), + new UiResultHandler(this) { + @Override + public void onResultUi(Boolean result) { + // TODO something? + } + }); } 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 long lastMsgTime = last.getTime(); long newMsgTime = item.getTime(); - if (newMsgTime > lastMsgTime) markNewMessageRead(item); - else loadMessages(); + if (newMsgTime > lastMsgTime) + conversationController.markNewMessageRead(item); } else { // 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) { - runOnDbThread(new Runnable() { - @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 messageIds, + @Override + public void markMessages(final Collection messageIds, final boolean sent, final boolean seen) { runOnUiThread(new Runnable() { @Override @@ -483,6 +354,12 @@ public class ConversationActivity extends BriarActivity }); } + @Override + public void messageReceived(ConversationItem item) { + addConversationItem(item); + markMessageReadIfNew(item); + } + @Override public void onClick(View view) { markMessagesRead(); @@ -501,37 +378,14 @@ public class ConversationActivity extends BriarActivity } private void createMessage(final byte[] body, final long timestamp) { - cryptoExecutor.execute(new Runnable() { - @Override - public void run() { - try { - storeMessage(privateMessageFactory - .createPrivateMessage(groupId, timestamp, null, - "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); - } - } - }); + conversationController.createMessage(body, timestamp, + new UiResultHandler(this) { + @Override + public void onResultUi(ConversationItem item) { + if (item != null) + addConversationItem(item); + } + }); } private void askToRemoveContact() { @@ -553,82 +407,42 @@ public class ConversationActivity extends BriarActivity } private void removeContact() { - runOnDbThread(new Runnable() { - @Override - public void run() { - try { - // make sure contactId is initialised - if (contactId == null) - contactId = conversationManager.getContactId(groupId); - // remove contact with that ID - contactManager.removeContact(contactId); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - } finally { - finishAfterContactRemoved(); - } - } - }); - } - - 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); + conversationController + .removeContact(new UiResultHandler(this) { + @Override + public void onResultUi(Boolean result) { + if (result) { + String deleted = + getString(R.string.contact_deleted_toast); + Toast.makeText(ConversationActivity.this, deleted, + LENGTH_SHORT) + .show(); + finish(); + } else { + String failed = getString( + R.string.contact_deletion_failed_toast); + Toast.makeText(ConversationActivity.this, failed, + LENGTH_SHORT).show(); + } } - } 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 - public void respondToItem(final ConversationItem item, - final boolean accept) { - runOnDbThread(new Runnable() { - @Override - public void run() { - long timestamp = System.currentTimeMillis(); - timestamp = Math.max(timestamp, getMinTimestampForNewMessage()); - try { - conversationManager - .respondToItem(contactId, item, accept, timestamp); - loadMessages(); - } catch (DbException | FormatException e) { - // TODO decide how to make this type-agnostic - introductionResponseError(); - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - } - } - }); + public void respondToItem(ConversationItem item, boolean accept) { + long minTimestamp = getMinTimestampForNewMessage(); + conversationController.respondToItem(item, accept, minTimestamp, + new UiResultHandler(this) { + @Override + public void onResultUi(Boolean result) { + if (result) { + loadMessages(); + } else { + // TODO decide how to make this type-agnostic + introductionResponseError(); + } + } + }); } private void introductionResponseError() { @@ -653,4 +467,9 @@ public class ConversationActivity extends BriarActivity } }); } + + @Override + public void contactUpdated() { + displayContactDetails(); + } } diff --git a/briar-android/src/org/briarproject/android/contact/ConversationController.java b/briar-android/src/org/briarproject/android/contact/ConversationController.java new file mode 100644 index 000000000..5afd594ec --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationController.java @@ -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 resultHandler); + + void loadMessages(UiResultHandler resultHandler); + + void createMessage(byte[] body, long timestamp, + UiResultHandler resultHandler); + + ContactId getContactId(); + + String getContactName(); + + byte[] getContactIdenticonKey(); + + List getConversationItems(); + + boolean isConnected(); + + void markMessagesRead(Collection unread, + UiResultHandler resultHandler); + + void markNewMessageRead(ConversationItem item); + + void removeContact(UiResultHandler resultHandler); + + void respondToItem(ConversationItem item, boolean accept, long minTimestamp, + UiResultHandler resultHandler); + + void shouldHideIntroductionAction(UiResultHandler resultHandler); + + interface ConversationListener { + void contactUpdated(); + + void markMessages(Collection messageIds, boolean sent, + boolean seen); + + void messageReceived(ConversationItem item); + } +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationControllerImpl.java b/briar-android/src/org/briarproject/android/contact/ConversationControllerImpl.java new file mode 100644 index 000000000..b6b8311d3 --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationControllerImpl.java @@ -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 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 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 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 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 getConversationItems() { + return data.getConversationItems(); + } + + @Override + public boolean isConnected() { + return data.isConnected(); + } + + @Override + public void markMessagesRead(final Collection unread, + final UiResultHandler 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 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 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 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(); + } + }); + } + } + } +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationPersistentData.java b/briar-android/src/org/briarproject/android/contact/ConversationPersistentData.java new file mode 100644 index 000000000..0ef4e1b07 --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationPersistentData.java @@ -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 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 items) { + this.items.addAll(items); + } + + public List getConversationItems() { + return Collections.unmodifiableList(items); + } +}