From a9b9a8c5f8374362e43dcfab9608a2943f7bea96 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 18 Oct 2019 11:32:15 -0300 Subject: [PATCH 1/7] [android] allow to select multiple conversation messages --- briar-android/build.gradle | 1 + .../conversation/ConversationActivity.java | 98 ++++++++++++++++++- .../conversation/ConversationAdapter.java | 26 ++++- .../conversation/ConversationItem.java | 6 ++ .../ConversationItemDetailsLookup.java | 53 ++++++++++ .../ConversationItemKeyProvider.java | 29 ++++++ .../ConversationItemViewHolder.java | 11 ++- .../ConversationMessageViewHolder.java | 4 +- .../ConversationNoticeViewHolder.java | 4 +- .../ConversationRequestViewHolder.java | 4 +- .../res/menu/conversation_message_actions.xml | 12 +++ briar-android/src/main/res/values/themes.xml | 1 + briar-android/witness.gradle | 1 + 13 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemDetailsLookup.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemKeyProvider.java create mode 100644 briar-android/src/main/res/menu/conversation_message_actions.xml 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..2e2c4533a 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 @@ -7,7 +7,9 @@ import android.os.Bundle; import android.os.Parcelable; import android.transition.Slide; import android.transition.Transition; +import android.util.Log; import android.util.SparseArray; +import android.view.ActionMode; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -98,13 +100,20 @@ 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; import static android.os.Build.VERSION.SDK_INT; import static android.view.Gravity.RIGHT; +import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_SHORT; import static androidx.core.app.ActivityOptionsCompat.makeSceneTransitionAnimation; import static androidx.core.view.ViewCompat.setTransitionName; @@ -120,6 +129,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 +147,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 +204,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 +270,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 +362,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 +427,84 @@ 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) { + Log.e("TEST", "selected: " + getSelection()); + Toast.makeText(this, "Not yet implemented", LENGTH_LONG).show(); + 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)) { 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..2c7e62b80 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 @@ -25,6 +25,8 @@ abstract class ConversationItemViewHolder extends ViewHolder { private final OutItemViewHolder outViewHolder; private final TextView text; protected final TextView time; + @Nullable + private String itemKey = null; ConversationItemViewHolder(View v, ConversationListener listener, boolean isIncoming) { @@ -37,7 +39,9 @@ abstract class ConversationItemViewHolder extends ViewHolder { } @CallSuper - void bind(ConversationItem item) { + void bind(ConversationItem item, boolean selected) { + itemKey = item.getKey(); + if (item.getText() != null) { text.setText(trim(item.getText())); } @@ -52,4 +56,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..626979ceb 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 @@ -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/menu/conversation_message_actions.xml b/briar-android/src/main/res/menu/conversation_message_actions.xml new file mode 100644 index 000000000..caf2d8ad9 --- /dev/null +++ b/briar-android/src/main/res/menu/conversation_message_actions.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file 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', From 9ce327a40cf3389001de116e45c8f6b6c60a23bb Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 18 Oct 2019 13:45:48 -0300 Subject: [PATCH 2/7] [android] highlight selected conversation messages in UI --- .../ConversationItemViewHolder.java | 3 + .../ConversationMessageViewHolder.java | 4 +- .../list_item_background_selectable.xml | 4 + .../layout/list_item_conversation_msg_in.xml | 79 +++---------------- .../list_item_conversation_msg_in_content.xml | 73 +++++++++++++++++ .../layout/list_item_conversation_msg_out.xml | 5 +- .../list_item_conversation_notice_in.xml | 5 +- .../list_item_conversation_notice_out.xml | 5 +- .../layout/list_item_conversation_request.xml | 8 +- briar-android/src/main/res/values/color.xml | 1 + 10 files changed, 107 insertions(+), 80 deletions(-) create mode 100644 briar-android/src/main/res/drawable/list_item_background_selectable.xml create mode 100644 briar-android/src/main/res/layout/list_item_conversation_msg_in_content.xml 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 2c7e62b80..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,6 +20,7 @@ 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; @@ -33,6 +34,7 @@ abstract class ConversationItemViewHolder extends ViewHolder { 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); @@ -41,6 +43,7 @@ abstract class ConversationItemViewHolder extends ViewHolder { @CallSuper void bind(ConversationItem item, boolean selected) { itemKey = item.getKey(); + root.setActivated(selected); if (item.getText() != null) { text.setText(trim(item.getText())); 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 626979ceb..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(), 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" /> #333333 @color/msg_stroke_light #66000000 + @color/briar_green @color/briar_blue_light From 97dd9b901d00aa895c315a6eb091f0166a592798 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 22 Oct 2019 11:38:10 -0300 Subject: [PATCH 3/7] [android] hook up UI to ConversationManager to actually delete messages --- .../conversation/ConversationActivity.java | 26 ++++++++++++++----- briar-android/src/main/res/values/strings.xml | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) 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 2e2c4533a..10e9e6be1 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 @@ -7,7 +7,6 @@ import android.os.Bundle; import android.os.Parcelable; import android.transition.Slide; import android.transition.Transition; -import android.util.Log; import android.util.SparseArray; import android.view.ActionMode; import android.view.Menu; @@ -113,7 +112,6 @@ import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt; import static android.os.Build.VERSION.SDK_INT; import static android.view.Gravity.RIGHT; -import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_SHORT; import static androidx.core.app.ActivityOptionsCompat.makeSceneTransitionAnimation; import static androidx.core.view.ViewCompat.setTransitionName; @@ -442,8 +440,7 @@ public class ConversationActivity extends BriarActivity @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { if (item.getItemId() == R.id.action_delete) { - Log.e("TEST", "selected: " + getSelection()); - Toast.makeText(this, "Not yet implemented", LENGTH_LONG).show(); + deleteSelectedMessages(); return true; } return false; @@ -862,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()); @@ -870,7 +867,24 @@ 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(); 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? From 5c900c443d47ebf1058b5b99344cbfa18207cacd Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 1 Nov 2019 12:46:12 -0300 Subject: [PATCH 4/7] [core] also support private messages in legacy format for selective deletion --- .../briar/messaging/MessagingManagerImpl.java | 22 +++++++++++----- .../MessagingManagerIntegrationTest.java | 26 +++++++++++++++++++ .../test/BriarIntegrationTestComponent.java | 3 +++ 3 files changed, 45 insertions(+), 6 deletions(-) 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..0bd93811a 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,23 @@ 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)); + // Date: 2019-11-01 + // When we remove support for old messages without MSG_KEY_MSG_TYPE, + // we can use a query here for (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 +459,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(); From 71243ce5617b89290e5f1fa575031b84f6eca1ea Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 7 Nov 2019 15:30:25 -0300 Subject: [PATCH 5/7] [android] prevent empty state message from showing up briefly when clearing list --- .../briar/android/conversation/ConversationActivity.java | 1 + 1 file changed, 1 insertion(+) 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 10e9e6be1..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 @@ -888,6 +888,7 @@ public class ConversationActivity extends BriarActivity boolean allDeleted) { runOnUiThreadUnlessDestroyed(() -> { adapter.clear(); + list.showProgressBar(); // otherwise clearing shows empty state loadMessages(); if (!allDeleted) showNotAllDeletedDialog(); }); From ddcb412fcdbd33913a69b71e8c69eae8e972aac3 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 7 Nov 2019 15:31:24 -0300 Subject: [PATCH 6/7] [core] remove notice about removing support for old message type --- .../org/briarproject/briar/messaging/MessagingManagerImpl.java | 3 --- 1 file changed, 3 deletions(-) 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 0bd93811a..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 @@ -372,9 +372,6 @@ class MessagingManagerImpl implements MessagingManager, IncomingMessageHook, public Set getMessageIds(Transaction txn, ContactId c) throws DbException { GroupId g = getContactGroup(db.getContact(txn, c)).getId(); - // Date: 2019-11-01 - // When we remove support for old messages without MSG_KEY_MSG_TYPE, - // we can use a query here for (MSG_KEY_MSG_TYPE, PRIVATE_MESSAGE) Set result = new HashSet<>(); try { Map messages = From 497ab38be1620ab092a59657a356a8176b21e3c0 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 8 Nov 2019 09:54:04 -0300 Subject: [PATCH 7/7] [android] highlight selected messages with accent color --- briar-android/src/main/res/values/color.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/briar-android/src/main/res/values/color.xml b/briar-android/src/main/res/values/color.xml index 2d84d131d..dd95be12c 100644 --- a/briar-android/src/main/res/values/color.xml +++ b/briar-android/src/main/res/values/color.xml @@ -42,7 +42,7 @@ #333333 @color/msg_stroke_light #66000000 - @color/briar_green + @color/briar_accent @color/briar_blue_light