From 3e6747c88083a0cf9653d6a095f0dd015ecf2deb Mon Sep 17 00:00:00 2001 From: Mishiranu Date: Sat, 17 Sep 2016 01:18:34 +0300 Subject: [PATCH 1/2] Add "Select text" context menu option --- .../ui/ConversationFragment.java | 60 +++++++++++++++++++ .../ui/adapter/MessageAdapter.java | 9 +++ src/main/res/menu/message_context.xml | 4 ++ src/main/res/values-ru/strings.xml | 1 + src/main/res/values/strings.xml | 1 + 5 files changed, 75 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index c6f6b8cd..da6f6da0 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -12,6 +12,8 @@ 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; @@ -38,6 +40,8 @@ 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; @@ -536,6 +540,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); @@ -548,6 +553,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); } if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { retryDecryption.setVisible(true); @@ -597,6 +603,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; @@ -656,6 +665,30 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } } + 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) { + 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) { + } + } + } + } + } + private void deleteFile(Message message) { if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) { message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); @@ -1430,4 +1463,31 @@ 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 9e2b3ba7..30a5dd6e 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -736,6 +736,15 @@ public class MessageAdapter extends ArrayAdapter { this.mUseGreenBackground = activity.useGreenBackground(); } + 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/res/menu/message_context.xml b/src/main/res/menu/message_context.xml index bc8acede..9bdbbcea 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"/> + Проверьте размер %1$s на %2$s Опции сообщения Копировать текст + Выбрать текст Копировать адрес ссылки Отправить ещё раз URL файла diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 22d89a81..1d53a278 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -362,6 +362,7 @@ Check %1$s size on %2$s Message options Copy text + Select text Copy original URL Send again File URL From 858a32729975abab31700162a447f7541e9a3000 Mon Sep 17 00:00:00 2001 From: Mishiranu Date: Sun, 18 Sep 2016 16:35:14 +0300 Subject: [PATCH 2/2] Retain TextView selection after list updating --- .../ui/ConversationFragment.java | 42 +--- .../ui/adapter/MessageAdapter.java | 12 ++ .../ui/widget/ListSelectionManager.java | 201 ++++++++++++++++++ 3 files changed, 216 insertions(+), 39 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java 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