From 268e2bcd5e71c43a5cb53fc966128dc19144c330 Mon Sep 17 00:00:00 2001 From: Christian Schneppe Date: Fri, 21 Oct 2016 21:54:15 +0200 Subject: Retain TextView selection after list updating --- .../pixart/messenger/ui/ConversationFragment.java | 26 ++- .../messenger/ui/adapter/MessageAdapter.java | 23 ++- .../messenger/ui/widget/ListSelectionManager.java | 201 +++++++++++++++++++++ src/main/res/menu/message_context.xml | 4 + src/main/res/values/strings.xml | 1 + 5 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 src/main/java/de/pixart/messenger/ui/widget/ListSelectionManager.java (limited to 'src') diff --git a/src/main/java/de/pixart/messenger/ui/ConversationFragment.java b/src/main/java/de/pixart/messenger/ui/ConversationFragment.java index 3dc37c653..68a4e887f 100644 --- a/src/main/java/de/pixart/messenger/ui/ConversationFragment.java +++ b/src/main/java/de/pixart/messenger/ui/ConversationFragment.java @@ -63,6 +63,7 @@ import de.pixart.messenger.ui.XmppActivity.OnValueEdited; import de.pixart.messenger.ui.adapter.MessageAdapter; import de.pixart.messenger.ui.adapter.MessageAdapter.OnContactPictureClicked; import de.pixart.messenger.ui.adapter.MessageAdapter.OnContactPictureLongClicked; +import de.pixart.messenger.ui.widget.ListSelectionManager; import de.pixart.messenger.utils.GeoHelper; import de.pixart.messenger.utils.UIHelper; import de.pixart.messenger.xmpp.XmppConnection; @@ -548,6 +549,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa activity.getMenuInflater().inflate(R.menu.message_context, menu); menu.setHeaderTitle(R.string.message_options); MenuItem copyText = menu.findItem(R.id.copy_text); + MenuItem selectText = menu.findItem(R.id.select_text); MenuItem retryDecryption = menu.findItem(R.id.retry_decryption); MenuItem correctMessage = menu.findItem(R.id.correct_message); MenuItem shareWith = menu.findItem(R.id.share_with); @@ -560,6 +562,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa && !GeoHelper.isGeoUri(m.getBody()) && m.treatAsDownloadable() != Message.Decision.MUST) { copyText.setVisible(true); + selectText.setVisible(ListSelectionManager.isSupported()); } if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { retryDecryption.setVisible(true); @@ -611,6 +614,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa case R.id.copy_text: copyText(selectedMessage); return true; + case R.id.select_text: + selectText(selectedMessage); + return true; case R.id.correct_message: correctMessage(selectedMessage); return true; @@ -670,7 +676,25 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } } - private void deleteFile(Message message) { + private void selectText(Message message) { + final int index; + synchronized (this.messageList) { + index = this.messageList.indexOf(message); + } + if (index >= 0) { + final int first = this.messagesView.getFirstVisiblePosition(); + final int last = first + this.messagesView.getChildCount(); + if (index >= first && index < last) { + final View view = this.messagesView.getChildAt(index - first); + final TextView messageBody = this.messageListAdapter.getMessageBody(view); + if (messageBody != null) { + ListSelectionManager.startSelection(messageBody); + } + } + } + } + + private void deleteFile(Message message) { if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) { message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); activity.updateConversationList(); diff --git a/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java b/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java index b53622984..6dc6a9ed1 100644 --- a/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java +++ b/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java @@ -57,6 +57,7 @@ import de.pixart.messenger.persistance.FileBackend; import de.pixart.messenger.ui.ConversationActivity; import de.pixart.messenger.ui.ShowFullscreenMessageActivity; import de.pixart.messenger.ui.widget.ClickableMovementMethod; +import de.pixart.messenger.ui.widget.ListSelectionManager; import de.pixart.messenger.utils.CryptoHelper; import de.pixart.messenger.utils.GeoHelper; import de.pixart.messenger.utils.UIHelper; @@ -83,6 +84,7 @@ public class MessageAdapter extends ArrayAdapter { private OnContactPictureLongClicked mOnContactPictureLongClickedListener; private boolean mIndicateReceived = false; + private final ListSelectionManager listSelectionManager = new ListSelectionManager(); private HashMap audioPlayer; private boolean mUseWhiteBackground = false; @@ -365,7 +367,9 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.messageBody.setText(formattedBody); viewHolder.messageBody.setTextIsSelectable(true); viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance()); - } else { + listSelectionManager.onUpdate(viewHolder.messageBody, message); + + } else { viewHolder.messageBody.setText(""); viewHolder.messageBody.setTextIsSelectable(false); } @@ -587,6 +591,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder = null; break; } + if (viewHolder.messageBody != null) listSelectionManager.onCreate(viewHolder.messageBody); view.setTag(viewHolder); if (viewHolder == null) { return view; @@ -743,6 +748,13 @@ public class MessageAdapter extends ArrayAdapter { return view; } + @Override + public void notifyDataSetChanged() { + listSelectionManager.onBeforeNotifyDataSetChanged(); + super.notifyDataSetChanged(); + listSelectionManager.onAfterNotifyDataSetChanged(); + } + public void openDownloadable(Message message) { DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); if (!file.exists()) { @@ -814,6 +826,15 @@ public class MessageAdapter extends ArrayAdapter { this.mUseWhiteBackground = activity.useWhiteBackground(); } + public TextView getMessageBody(View view) { + final Object tag = view.getTag(); + if (tag instanceof ViewHolder) { + final ViewHolder viewHolder = (ViewHolder) tag; + return viewHolder.messageBody; + } + return null; + } + public interface OnContactPictureClicked { void onContactPictureClicked(Message message); } diff --git a/src/main/java/de/pixart/messenger/ui/widget/ListSelectionManager.java b/src/main/java/de/pixart/messenger/ui/widget/ListSelectionManager.java new file mode 100644 index 000000000..4cf1a9e42 --- /dev/null +++ b/src/main/java/de/pixart/messenger/ui/widget/ListSelectionManager.java @@ -0,0 +1,201 @@ +package de.pixart.messenger.ui.widget; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.text.Selection; +import android.text.Spannable; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.TextView; + +public class ListSelectionManager { + + private static final int MESSAGE_SEND_RESET = 1; + private static final int MESSAGE_RESET = 2; + private static final int MESSAGE_START_SELECTION = 3; + + private static final Handler HANDLER = new Handler(Looper.getMainLooper(), new Handler.Callback() { + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_SEND_RESET: { + // Skip one more message queue loop + HANDLER.obtainMessage(MESSAGE_RESET, msg.obj).sendToTarget(); + return true; + } + case MESSAGE_RESET: { + final ListSelectionManager listSelectionManager = (ListSelectionManager) msg.obj; + listSelectionManager.futureSelectionIdentifier = null; + return true; + } + case MESSAGE_START_SELECTION: { + final StartSelectionHolder holder = (StartSelectionHolder) msg.obj; + holder.listSelectionManager.futureSelectionIdentifier = null; + startSelection(holder.textView, holder.start, holder.end); + return true; + } + } + return false; + } + }); + + private static class StartSelectionHolder { + + public final ListSelectionManager listSelectionManager; + public final TextView textView; + public final int start; + public final int end; + + public StartSelectionHolder(ListSelectionManager listSelectionManager, TextView textView, + int start, int end) { + this.listSelectionManager = listSelectionManager; + this.textView = textView; + this.start = start; + this.end = end; + } + } + + private ActionMode selectionActionMode; + private Object selectionIdentifier; + private TextView selectionTextView; + + private Object futureSelectionIdentifier; + private int futureSelectionStart; + private int futureSelectionEnd; + + public void onCreate(TextView textView) { + final CustomCallback callback = new CustomCallback(textView); + textView.setCustomSelectionActionModeCallback(callback); + } + + public void onUpdate(TextView textView, Object identifier) { + if (SUPPORTED) { + CustomCallback callback = (CustomCallback) textView.getCustomSelectionActionModeCallback(); + callback.identifier = identifier; + if (futureSelectionIdentifier == identifier) { + HANDLER.obtainMessage(MESSAGE_START_SELECTION, new StartSelectionHolder(this, + textView, futureSelectionStart, futureSelectionEnd)).sendToTarget(); + } + } + } + + public void onBeforeNotifyDataSetChanged() { + if (SUPPORTED) { + HANDLER.removeMessages(MESSAGE_SEND_RESET); + HANDLER.removeMessages(MESSAGE_RESET); + HANDLER.removeMessages(MESSAGE_START_SELECTION); + if (selectionActionMode != null) { + final CharSequence text = selectionTextView.getText(); + futureSelectionIdentifier = selectionIdentifier; + futureSelectionStart = Selection.getSelectionStart(text); + futureSelectionEnd = Selection.getSelectionEnd(text); + selectionActionMode.finish(); + selectionActionMode = null; + selectionIdentifier = null; + selectionTextView = null; + } + } + } + + public void onAfterNotifyDataSetChanged() { + if (SUPPORTED && futureSelectionIdentifier != null) { + HANDLER.obtainMessage(MESSAGE_SEND_RESET, this).sendToTarget(); + } + } + + private class CustomCallback implements ActionMode.Callback { + + private final TextView textView; + public Object identifier; + + public CustomCallback(TextView textView) { + this.textView = textView; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + selectionActionMode = mode; + selectionIdentifier = identifier; + selectionTextView = textView; + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return true; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + if (selectionActionMode == mode) { + selectionActionMode = null; + selectionIdentifier = null; + selectionTextView = null; + } + } + } + + private static final Field FIELD_EDITOR; + private static final Method METHOD_START_SELECTION; + private static final boolean SUPPORTED; + + static { + Field editor; + try { + editor = TextView.class.getDeclaredField("mEditor"); + editor.setAccessible(true); + } catch (Exception e) { + editor = null; + } + FIELD_EDITOR = editor; + Method startSelection = null; + if (editor != null) { + String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"}; + for (String startSelectionName : startSelectionNames) { + try { + startSelection = editor.getType().getDeclaredMethod(startSelectionName); + startSelection.setAccessible(true); + break; + } catch (Exception e) { + startSelection = null; + } + } + } + METHOD_START_SELECTION = startSelection; + SUPPORTED = FIELD_EDITOR != null && METHOD_START_SELECTION != null; + } + + public static boolean isSupported() { + return SUPPORTED; + } + + public static void startSelection(TextView textView) { + startSelection(textView, 0, textView.getText().length()); + } + + public static void startSelection(TextView textView, int start, int end) { + final CharSequence text = textView.getText(); + if (SUPPORTED && start >= 0 && end > start && textView.isTextSelectable() && text instanceof Spannable) { + final Spannable spannable = (Spannable) text; + start = Math.min(start, spannable.length()); + end = Math.min(end, spannable.length()); + Selection.setSelection(spannable, start, end); + try { + final Object editor = FIELD_EDITOR != null ? FIELD_EDITOR.get(textView) : textView; + METHOD_START_SELECTION.invoke(editor); + } catch (Exception e) { + } + } + } +} \ No newline at end of file diff --git a/src/main/res/menu/message_context.xml b/src/main/res/menu/message_context.xml index bc8acede0..a17c8c459 100644 --- a/src/main/res/menu/message_context.xml +++ b/src/main/res/menu/message_context.xml @@ -5,6 +5,10 @@ android:id="@+id/copy_text" android:title="@string/copy_text" android:visible="false"/> + Missing presence subscription Missing OMEMO keys You need to be connected to publish your avatar. + Select text -- cgit v1.2.3