[android] allow to select multiple conversation messages

This commit is contained in:
Torsten Grote
2019-10-18 11:32:15 -03:00
parent ed66a470cc
commit a9b9a8c5f8
13 changed files with 239 additions and 11 deletions

View File

@@ -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)) {

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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;
}
};
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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()) {

View File

@@ -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)) {

View File

@@ -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);