diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index da6f6da0..14469355 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -12,8 +12,6 @@ import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.os.Handler; import android.text.InputType; -import android.text.Selection; -import android.text.Spannable; import android.util.Log; import android.util.Pair; import android.view.ContextMenu; @@ -40,8 +38,6 @@ import android.widget.Toast; import net.java.otr4j.session.SessionStatus; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -66,6 +62,7 @@ import eu.siacs.conversations.ui.XmppActivity.OnValueEdited; import eu.siacs.conversations.ui.adapter.MessageAdapter; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked; +import eu.siacs.conversations.ui.widget.ListSelectionManager; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.XmppConnection; @@ -553,7 +550,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa && !GeoHelper.isGeoUri(m.getBody()) && m.treatAsDownloadable() != Message.Decision.MUST) { copyText.setVisible(true); - selectText.setVisible(METHOD_START_SELECTION != null); + selectText.setVisible(ListSelectionManager.isSupported()); } if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { retryDecryption.setVisible(true); @@ -677,13 +674,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa final View view = this.messagesView.getChildAt(index - first); final TextView messageBody = this.messageListAdapter.getMessageBody(view); if (messageBody != null) { - final Spannable text = (Spannable) messageBody.getText(); - Selection.setSelection(text, 0, text.length()); - try { - Object editor = FIELD_EDITOR != null ? FIELD_EDITOR.get(messageBody) : messageBody; - METHOD_START_SELECTION.invoke(editor); - } catch (Exception e) { - } + ListSelectionManager.startSelection(messageBody); } } } @@ -1463,31 +1454,4 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } } - private static final Field FIELD_EDITOR; - private static final Method METHOD_START_SELECTION; - - static { - Field editor; - try { - editor = TextView.class.getDeclaredField("mEditor"); - editor.setAccessible(true); - } catch (Exception e) { - editor = null; - } - FIELD_EDITOR = editor; - Class editorClass = editor != null ? editor.getType() : TextView.class; - String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"}; - Method startSelection = null; - for (String startSelectionName : startSelectionNames) { - try { - startSelection = editorClass.getDeclaredMethod(startSelectionName); - startSelection.setAccessible(true); - break; - } catch (Exception e) { - startSelection = null; - } - } - METHOD_START_SELECTION = startSelection; - } - } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 30a5dd6e..b24c9539 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -53,6 +53,7 @@ import eu.siacs.conversations.entities.Message.FileParams; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.widget.ClickableMovementMethod; +import eu.siacs.conversations.ui.widget.ListSelectionManager; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.UIHelper; @@ -86,6 +87,8 @@ public class MessageAdapter extends ArrayAdapter { private boolean mIndicateReceived = false; private boolean mUseGreenBackground = false; + private final ListSelectionManager listSelectionManager = new ListSelectionManager(); + public MessageAdapter(ConversationActivity activity, List messages) { super(activity, 0, messages); this.activity = activity; @@ -361,6 +364,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.messageBody.setText(formattedBody); viewHolder.messageBody.setTextIsSelectable(true); viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance()); + listSelectionManager.onUpdate(viewHolder.messageBody, message); } else { viewHolder.messageBody.setText(""); viewHolder.messageBody.setTextIsSelectable(false); @@ -534,6 +538,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder = null; break; } + if (viewHolder.messageBody != null) listSelectionManager.onCreate(viewHolder.messageBody); view.setTag(viewHolder); } else { viewHolder = (ViewHolder) view.getTag(); @@ -684,6 +689,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()) { diff --git a/src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java b/src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java new file mode 100644 index 00000000..9e256448 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java @@ -0,0 +1,201 @@ +package eu.siacs.conversations.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