mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-11 18:29:05 +01:00
[android] allow to select multiple conversation messages
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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<String> 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<String> observer = new SelectionObserver<String>() {
|
||||
@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<MessageId> getSelection() {
|
||||
Selection<String> selection = tracker.getSelection();
|
||||
List<MessageId> 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)) {
|
||||
|
||||
@@ -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<ConversationItem, ConversationItemViewHolder>
|
||||
@@ -27,6 +29,8 @@ class ConversationAdapter
|
||||
private ConversationListener listener;
|
||||
private final RecycledViewPool imageViewPool;
|
||||
private final ImageItemDecoration imageItemDecoration;
|
||||
@Nullable
|
||||
private SelectionTracker<String> 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<String> tracker) {
|
||||
this.tracker = tracker;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
ConversationItem getLastItem() {
|
||||
if (items.size() > 0) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<String > {
|
||||
|
||||
private final RecyclerView recyclerView;
|
||||
|
||||
ConversationItemDetailsLookup(RecyclerView recyclerView) {
|
||||
this.recyclerView = recyclerView;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ItemDetails<String> 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<String>() {
|
||||
@Override
|
||||
public int getPosition() {
|
||||
return pos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSelectionKey() {
|
||||
return id;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:icon="@drawable/action_delete_white"
|
||||
android:title="@string/delete"
|
||||
app:showAsAction="always"/>
|
||||
|
||||
</menu>
|
||||
@@ -8,6 +8,7 @@
|
||||
<item name="android:textColorLink">@color/briar_text_link</item>
|
||||
<item name="android:windowBackground">@color/window_background</item>
|
||||
<item name="android:windowAnimationStyle">@style/ActivityAnimation</item>
|
||||
<item name="windowActionModeOverlay">true</item>
|
||||
<item name="alertDialogTheme">@style/BriarDialogTheme.Neutral</item>
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
||||
</style>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user