diff --git a/briar-android/build.gradle b/briar-android/build.gradle index 6d2fb419e..34040fa1f 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -97,6 +97,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'com.google.android.material:material:1.1.0-beta01' + implementation 'androidx.recyclerview:recyclerview-selection:1.0.0' implementation 'ch.acra:acra:4.11' implementation 'info.guardianproject.panic:panic:1.0' diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 349e84c82..dc48c59f9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -8,6 +8,7 @@ import android.os.Parcelable; import android.transition.Slide; import android.transition.Transition; import android.util.SparseArray; +import android.view.ActionMode; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -98,7 +99,13 @@ import androidx.core.content.ContextCompat; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.selection.Selection; +import androidx.recyclerview.selection.SelectionPredicates; +import androidx.recyclerview.selection.SelectionTracker; +import androidx.recyclerview.selection.SelectionTracker.SelectionObserver; +import androidx.recyclerview.selection.StorageStrategy; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import de.hdodenhof.circleimageview.CircleImageView; import im.delight.android.identicons.IdenticonDrawable; import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt; @@ -120,6 +127,7 @@ import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.now; +import static org.briarproject.bramble.util.StringUtils.fromHexString; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ATTACH_IMAGE; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION; @@ -137,7 +145,7 @@ import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVAT @ParametersNotNullByDefault public class ConversationActivity extends BriarActivity implements EventListener, ConversationListener, TextCache, - AttachmentCache, AttachmentListener { + AttachmentCache, AttachmentListener, ActionMode.Callback { public static final String CONTACT_ID = "briar.CONTACT_ID"; @@ -194,8 +202,11 @@ public class ConversationActivity extends BriarActivity private LinearLayoutManager layoutManager; private TextInputView textInputView; private TextSendController sendController; + private SelectionTracker tracker; @Nullable private Parcelable layoutManagerState; + @Nullable + private ActionMode actionMode; private volatile ContactId contactId; @@ -257,6 +268,9 @@ public class ConversationActivity extends BriarActivity ConversationScrollListener scrollListener = new ConversationScrollListener(adapter, viewModel); list.getRecyclerView().addOnScrollListener(scrollListener); + if (featureFlags.shouldEnablePrivateMessageDeletion()) { + addSelectionTracker(); + } textInputView = findViewById(R.id.text_input_container); if (featureFlags.shouldEnableImageAttachments()) { @@ -346,12 +360,14 @@ public class ConversationActivity extends BriarActivity layoutManagerState = layoutManager.onSaveInstanceState(); outState.putParcelable("layoutManager", layoutManagerState); } + if (tracker != null) tracker.onSaveInstanceState(outState); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); layoutManagerState = savedInstanceState.getParcelable("layoutManager"); + if (tracker != null) tracker.onRestoreInstanceState(savedInstanceState); } @Override @@ -409,6 +425,83 @@ public class ConversationActivity extends BriarActivity } } + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.conversation_message_actions, menu); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; // no update needed + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (item.getItemId() == R.id.action_delete) { + deleteSelectedMessages(); + return true; + } + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + tracker.clearSelection(); + actionMode = null; + } + + private void addSelectionTracker() { + RecyclerView recyclerView = list.getRecyclerView(); + if (recyclerView.getAdapter() != adapter) + throw new IllegalStateException(); + + tracker = new SelectionTracker.Builder<>( + "conversationSelection", + recyclerView, + new ConversationItemKeyProvider(adapter), + new ConversationItemDetailsLookup(recyclerView), + StorageStrategy.createStringStorage() + ).withSelectionPredicate( + SelectionPredicates.createSelectAnything() + ).build(); + + SelectionObserver observer = new SelectionObserver() { + @Override + public void onItemStateChanged(String key, boolean selected) { + if (selected && actionMode == null) { + actionMode = startActionMode(ConversationActivity.this); + updateActionModeTitle(); + } else if (actionMode != null) { + if (selected || tracker.hasSelection()) { + updateActionModeTitle(); + } else { + actionMode.finish(); + } + } + } + }; + tracker.addObserver(observer); + adapter.setSelectionTracker(tracker); + } + + private void updateActionModeTitle() { + if (actionMode == null) throw new IllegalStateException(); + String title = String.valueOf(tracker.getSelection().size()); + actionMode.setTitle(title); + } + + private Collection getSelection() { + Selection selection = tracker.getSelection(); + List messages = new ArrayList<>(selection.size()); + for (String str : selection) { + MessageId id = new MessageId(fromHexString(str)); + messages.add(id); + } + return messages; + } + @UiThread private void displayContactOnlineStatus() { if (connectionRegistry.isConnected(contactId)) { @@ -766,7 +859,7 @@ public class ConversationActivity extends BriarActivity try { boolean allDeleted = conversationManager.deleteAllMessages(contactId); - reloadConversationAfterDeletingAllMessages(allDeleted); + reloadConversationAfterDeletingMessages(allDeleted); } catch (DbException e) { logException(LOG, WARNING, e); runOnUiThreadUnlessDestroyed(() -> list.showData()); @@ -774,10 +867,28 @@ public class ConversationActivity extends BriarActivity }); } - private void reloadConversationAfterDeletingAllMessages( + private void deleteSelectedMessages() { + list.showProgressBar(); + Collection selected = getSelection(); + // close action mode only after getting the selection + if (actionMode != null) actionMode.finish(); + runOnDbThread(() -> { + try { + boolean allDeleted = + conversationManager.deleteMessages(contactId, selected); + reloadConversationAfterDeletingMessages(allDeleted); + } catch (DbException e) { + logException(LOG, WARNING, e); + runOnUiThreadUnlessDestroyed(() -> list.showData()); + } + }); + } + + private void reloadConversationAfterDeletingMessages( boolean allDeleted) { runOnUiThreadUnlessDestroyed(() -> { adapter.clear(); + list.showProgressBar(); // otherwise clearing shows empty state loadMessages(); if (!allDeleted) showNotAllDeletedDialog(); }); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java index 13bde921a..1e7065df7 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java @@ -13,12 +13,14 @@ import org.briarproject.briar.R; import org.briarproject.briar.android.util.BriarAdapter; import org.briarproject.briar.android.util.ItemReturningAdapter; -import javax.annotation.Nullable; - import androidx.annotation.LayoutRes; +import androidx.annotation.Nullable; +import androidx.recyclerview.selection.SelectionTracker; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView.RecycledViewPool; +import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; + @NotNullByDefault class ConversationAdapter extends BriarAdapter @@ -27,6 +29,8 @@ class ConversationAdapter private ConversationListener listener; private final RecycledViewPool imageViewPool; private final ImageItemDecoration imageItemDecoration; + @Nullable + private SelectionTracker tracker = null; ConversationAdapter(Context ctx, ConversationListener conversationListener) { @@ -45,6 +49,17 @@ class ConversationAdapter return item.getLayout(); } + String getItemKey(int position) { + return items.get(position).getKey(); + } + + int getPositionOfKey(String key) { + for (int i = 0; i < items.size(); i++) { + if (key.equals(items.get(i).getKey())) return i; + } + return NO_POSITION; + } + @Override public ConversationItemViewHolder onCreateViewHolder(ViewGroup viewGroup, @LayoutRes int type) { @@ -71,7 +86,8 @@ class ConversationAdapter @Override public void onBindViewHolder(ConversationItemViewHolder ui, int position) { ConversationItem item = items.get(position); - ui.bind(item); + boolean selected = tracker != null && tracker.isSelected(item.getKey()); + ui.bind(item, selected); } @Override @@ -91,6 +107,10 @@ class ConversationAdapter return c1.equals(c2); } + void setSelectionTracker(SelectionTracker tracker) { + this.tracker = tracker; + } + @Nullable ConversationItem getLastItem() { if (items.size() > 0) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItem.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItem.java index f70ce35bc..a0cd5d54b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItem.java @@ -10,6 +10,8 @@ import javax.annotation.concurrent.NotThreadSafe; import androidx.annotation.LayoutRes; +import static org.briarproject.bramble.util.StringUtils.toHexString; + @NotThreadSafe @NotNullByDefault abstract class ConversationItem { @@ -45,6 +47,10 @@ abstract class ConversationItem { return id; } + String getKey() { + return toHexString(id.getBytes()); + } + GroupId getGroupId() { return groupId; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemDetailsLookup.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemDetailsLookup.java new file mode 100644 index 000000000..021c769c2 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemDetailsLookup.java @@ -0,0 +1,53 @@ +package org.briarproject.briar.android.conversation; + +import android.view.MotionEvent; +import android.view.View; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import androidx.annotation.Nullable; +import androidx.recyclerview.selection.ItemDetailsLookup; +import androidx.recyclerview.widget.RecyclerView; + +import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; + +@NotNullByDefault +class ConversationItemDetailsLookup extends ItemDetailsLookup { + + private final RecyclerView recyclerView; + + ConversationItemDetailsLookup(RecyclerView recyclerView) { + this.recyclerView = recyclerView; + } + + @Nullable + @Override + public ItemDetails getItemDetails(MotionEvent e) { + // find view that corresponds to MotionEvent + View view = recyclerView.findChildViewUnder(e.getX(), e.getY()); + if (view == null) return null; + + // get position + int pos = recyclerView.getChildAdapterPosition(view); + if (pos == NO_POSITION) return null; + + // get key ID + ConversationItemViewHolder holder = + (ConversationItemViewHolder) recyclerView.getChildViewHolder(view); + String id = holder.getItemKey(); + if (id == null) return null; + + return new ItemDetails() { + @Override + public int getPosition() { + return pos; + } + + @Override + public String getSelectionKey() { + return id; + } + }; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemKeyProvider.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemKeyProvider.java new file mode 100644 index 000000000..8e611400c --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemKeyProvider.java @@ -0,0 +1,29 @@ +package org.briarproject.briar.android.conversation; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import androidx.annotation.Nullable; +import androidx.recyclerview.selection.ItemKeyProvider; + +@NotNullByDefault +class ConversationItemKeyProvider extends ItemKeyProvider { + + private final ConversationAdapter adapter; + + protected ConversationItemKeyProvider(ConversationAdapter adapter) { + super(SCOPE_MAPPED); + this.adapter = adapter; + } + + @Nullable + @Override + public String getKey(int position) { + return adapter.getItemKey(position); + } + + @Override + public int getPosition(String key) { + return adapter.getPositionOfKey(key); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java index 77b1b9156..6a54584b4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java @@ -20,24 +20,31 @@ import static org.briarproject.briar.android.util.UiUtils.formatDate; abstract class ConversationItemViewHolder extends ViewHolder { protected final ConversationListener listener; + private final View root; protected final ConstraintLayout layout; @Nullable private final OutItemViewHolder outViewHolder; private final TextView text; protected final TextView time; + @Nullable + private String itemKey = null; ConversationItemViewHolder(View v, ConversationListener listener, boolean isIncoming) { super(v); this.listener = listener; this.outViewHolder = isIncoming ? null : new OutItemViewHolder(v); + root = v; layout = v.findViewById(R.id.layout); text = v.findViewById(R.id.text); time = v.findViewById(R.id.time); } @CallSuper - void bind(ConversationItem item) { + void bind(ConversationItem item, boolean selected) { + itemKey = item.getKey(); + root.setActivated(selected); + if (item.getText() != null) { text.setText(trim(item.getText())); } @@ -52,4 +59,9 @@ abstract class ConversationItemViewHolder extends ViewHolder { return outViewHolder == null; } + @Nullable + String getItemKey() { + return itemKey; + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java index 4d5f11561..4d6fd43e4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java @@ -44,8 +44,8 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { timeColorBubble = getColor(v.getContext(), R.color.briar_white); // clone constraint sets from layout files - textConstraints - .clone(v.getContext(), R.layout.list_item_conversation_msg_in); + textConstraints.clone(v.getContext(), + R.layout.list_item_conversation_msg_in_content); imageConstraints.clone(v.getContext(), R.layout.list_item_conversation_msg_image); imageTextConstraints.clone(v.getContext(), @@ -61,8 +61,8 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { } @Override - void bind(ConversationItem conversationItem) { - super.bind(conversationItem); + void bind(ConversationItem conversationItem, boolean selected) { + super.bind(conversationItem, selected); ConversationMessageItem item = (ConversationMessageItem) conversationItem; if (item.getAttachments().isEmpty()) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeViewHolder.java index 1e70f0560..2244b7ca8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeViewHolder.java @@ -28,9 +28,9 @@ class ConversationNoticeViewHolder extends ConversationItemViewHolder { @Override @CallSuper - void bind(ConversationItem item) { + void bind(ConversationItem item, boolean selected) { ConversationNoticeItem notice = (ConversationNoticeItem) item; - super.bind(notice); + super.bind(notice, selected); String text = notice.getMsgText(); if (isNullOrEmpty(text)) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestViewHolder.java index 3792ee0dc..d5f578339 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestViewHolder.java @@ -26,9 +26,9 @@ class ConversationRequestViewHolder extends ConversationNoticeViewHolder { } @Override - void bind(ConversationItem item) { + void bind(ConversationItem item, boolean selected) { ConversationRequestItem request = (ConversationRequestItem) item; - super.bind(request); + super.bind(request, selected); if (request.wasAnswered() && request.canBeOpened()) { acceptButton.setVisibility(VISIBLE); diff --git a/briar-android/src/main/res/drawable/list_item_background_selectable.xml b/briar-android/src/main/res/drawable/list_item_background_selectable.xml new file mode 100644 index 000000000..7d4aa9942 --- /dev/null +++ b/briar-android/src/main/res/drawable/list_item_background_selectable.xml @@ -0,0 +1,4 @@ + + + + diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml index a92cf249c..d946046e4 100644 --- a/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml +++ b/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml @@ -1,73 +1,14 @@ - + android:background="@drawable/list_item_background_selectable"> - + + - - - - - - - - - + diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_in_content.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_in_content.xml new file mode 100644 index 000000000..a92cf249c --- /dev/null +++ b/briar-android/src/main/res/layout/list_item_conversation_msg_in_content.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml index f77f9609b..6ac5ef61a 100644 --- a/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml +++ b/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml @@ -1,9 +1,10 @@ - + + android:layout_height="wrap_content" + android:background="@drawable/list_item_background_selectable"> + android:background="@drawable/list_item_background_selectable" + android:orientation="vertical" + android:paddingTop="@dimen/message_bubble_margin"> + android:background="@drawable/list_item_background_selectable" + android:orientation="vertical" + android:paddingTop="@dimen/message_bubble_margin"> + android:background="@drawable/list_item_background_selectable" + android:orientation="vertical" + android:paddingTop="@dimen/message_bubble_margin"> + tools:text="Short message" + tools:visibility="visible" /> + + + + + \ No newline at end of file diff --git a/briar-android/src/main/res/values/color.xml b/briar-android/src/main/res/values/color.xml index 8f4226d8f..dd95be12c 100644 --- a/briar-android/src/main/res/values/color.xml +++ b/briar-android/src/main/res/values/color.xml @@ -42,6 +42,7 @@ #333333 @color/msg_stroke_light #66000000 + @color/briar_accent @color/briar_blue_light diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 5e32aaa6c..61c21bdd6 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -139,7 +139,7 @@ Confirm Message Deletion Are you sure that you want to delete all messages? Could not delete all messages - Messages related to ongoing introductions or invitations cannot be deleted until they conclude. + Messages related to\n\n• ongoing introductions\n• ongoing (blog/forum/group) invitations\n• partly downloaded messages\n\ncannot be deleted until they conclude. Delete contact Confirm Contact Deletion Are you sure that you want to remove this contact and all messages exchanged with this contact? diff --git a/briar-android/src/main/res/values/themes.xml b/briar-android/src/main/res/values/themes.xml index e47e9d233..211fe6ebc 100644 --- a/briar-android/src/main/res/values/themes.xml +++ b/briar-android/src/main/res/values/themes.xml @@ -8,6 +8,7 @@ @color/briar_text_link @color/window_background @style/ActivityAnimation + true @style/BriarDialogTheme.Neutral @style/PreferenceThemeOverlay.v14.Material diff --git a/briar-android/witness.gradle b/briar-android/witness.gradle index 5569b2788..5824b4577 100644 --- a/briar-android/witness.gradle +++ b/briar-android/witness.gradle @@ -36,6 +36,7 @@ dependencyVerification { 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0:localbroadcastmanager-1.0.0.aar:e71c328ceef5c4a7d76f2d86df1b65d65fe2acf868b1a4efd84a3f34336186d8', 'androidx.preference:preference:1.1.0:preference-1.1.0.aar:6cf1a099b03b3254041b04701841865b2708c0b546b9036c8b0dada0aa59de57', 'androidx.print:print:1.0.0:print-1.0.0.aar:1d5c7f3135a1bba661fc373fd72e11eb0a4adbb3396787826dd8e4190d5d9edd', + 'androidx.recyclerview:recyclerview-selection:1.0.0:recyclerview-selection-1.0.0.aar:db3db72af8cfcd701ab6ed7a080327a2e993e3a429f5efb8f0c108bff38c4922', 'androidx.recyclerview:recyclerview:1.1.0-beta04:recyclerview-1.1.0-beta04.aar:c3c8310eb058a660a845cf814a54f56e6f448b7855f9ccea2a5ad18189f57f69', 'androidx.savedstate:savedstate:1.0.0:savedstate-1.0.0.aar:2510a5619c37579c9ce1a04574faaf323cd0ffe2fc4e20fa8f8f01e5bb402e83', 'androidx.test.espresso:espresso-contrib:3.2.0:espresso-contrib-3.2.0.aar:9e43811e5845e84f2964f0032fd50cd11dca3dc8d3b0703626dd12d50433bb35', diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java index 0003b8159..36072a4d7 100644 --- a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java @@ -47,6 +47,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -371,15 +372,20 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook, public Set getMessageIds(Transaction txn, ContactId c) throws DbException { GroupId g = getContactGroup(db.getContact(txn, c)).getId(); - BdfDictionary query = BdfDictionary.of( - new BdfEntry(MSG_KEY_MSG_TYPE, PRIVATE_MESSAGE)); + Set result = new HashSet<>(); try { - Map results = - clientHelper.getMessageMetadataAsDictionary(txn, g, query); - return results.keySet(); + Map messages = + clientHelper.getMessageMetadataAsDictionary(txn, g); + for (Map.Entry entry : messages + .entrySet()) { + Long type = entry.getValue().getOptionalLong(MSG_KEY_MSG_TYPE); + if (type == null || type == PRIVATE_MESSAGE) + result.add(entry.getKey()); + } } catch (FormatException e) { throw new DbException(e); } + return result; } @Override @@ -450,7 +456,8 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook, Long messageType = meta.getOptionalLong(MSG_KEY_MSG_TYPE); if (messageType != null && messageType != PRIVATE_MESSAGE) throw new AssertionError("not supported"); - headers = parseAttachmentHeaders(meta); + headers = messageType == null ? emptyList() : + parseAttachmentHeaders(meta); } catch (FormatException e) { throw new DbException(e); } diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/MessagingManagerIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/messaging/MessagingManagerIntegrationTest.java index 211e7c060..f5cf206f9 100644 --- a/briar-core/src/test/java/org/briarproject/briar/messaging/MessagingManagerIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/messaging/MessagingManagerIntegrationTest.java @@ -208,6 +208,32 @@ public class MessagingManagerIntegrationTest assertGroupCounts(c0, 0, 0); } + @Test + public void testDeleteLegacySubset() throws Exception { + // send legacy message + GroupId g = c0.getMessagingManager().getConversationId(contactId); + PrivateMessage m0 = messageFactory.createLegacyPrivateMessage(g, + clock.currentTimeMillis(), getRandomString(42)); + c0.getMessagingManager().addLocalMessage(m0); + syncMessage(c0, c1, contactId, 1, true); + + // message arrived on both sides + assertEquals(1, getMessages(c0).size()); + assertEquals(1, getMessages(c1).size()); + + // delete message on both sides (deletes all, because returns true) + Set toDelete = new HashSet<>(); + toDelete.add(m0.getMessage().getId()); + assertTrue(c0.getConversationManager() + .deleteMessages(contactId, toDelete)); + assertTrue(c1.getConversationManager() + .deleteMessages(contactId, toDelete)); + + // message was deleted + assertEquals(0, getMessages(c0).size()); + assertEquals(0, getMessages(c1).size()); + } + @Test public void testDeleteAttachment() throws Exception { // send one message with attachment diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java index 6c3b92aae..0eebc1a4f 100644 --- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java +++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java @@ -16,6 +16,7 @@ import org.briarproject.briar.api.blog.BlogFactory; import org.briarproject.briar.api.blog.BlogManager; import org.briarproject.briar.api.blog.BlogSharingManager; import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.conversation.ConversationManager; import org.briarproject.briar.api.forum.ForumManager; import org.briarproject.briar.api.forum.ForumSharingManager; import org.briarproject.briar.api.introduction.IntroductionManager; @@ -89,6 +90,8 @@ public interface BriarIntegrationTestComponent ContactManager getContactManager(); + ConversationManager getConversationManager(); + DatabaseComponent getDatabaseComponent(); BlogManager getBlogManager();