aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationFragment.java24
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java21
-rw-r--r--src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java201
-rw-r--r--src/main/res/menu/message_context.xml4
-rw-r--r--src/main/res/values-ru/strings.xml1
-rw-r--r--src/main/res/values/strings.xml1
6 files changed, 252 insertions, 0 deletions
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index 3490a712..df01e87c 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -62,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;
@@ -536,6 +537,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 +550,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);
@@ -598,6 +601,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;
@@ -657,6 +663,24 @@ 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) {
+ ListSelectionManager.startSelection(messageBody);
+ }
+ }
+ }
+ }
+
private void deleteFile(Message message) {
if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) {
message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
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 496d8ab5..4505e632 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
@@ -54,6 +54,7 @@ import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.persistance.FileBackend;
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;
@@ -87,6 +88,8 @@ public class MessageAdapter extends ArrayAdapter<Message> {
private boolean mIndicateReceived = false;
private boolean mUseGreenBackground = false;
+ private final ListSelectionManager listSelectionManager = new ListSelectionManager();
+
public MessageAdapter(ConversationActivity activity, List<Message> messages) {
super(activity, 0, messages);
this.activity = activity;
@@ -362,6 +365,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
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);
@@ -535,6 +539,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder = null;
break;
}
+ if (viewHolder.messageBody != null) listSelectionManager.onCreate(viewHolder.messageBody);
view.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) view.getTag();
@@ -685,6 +690,13 @@ public class MessageAdapter extends ArrayAdapter<Message> {
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()) {
@@ -737,6 +749,15 @@ public class MessageAdapter extends ArrayAdapter<Message> {
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/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
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"/>
+ <item
+ android:id="@+id/select_text"
+ android:title="@string/select_text"
+ android:visible="false"/>
<item
android:id="@+id/retry_decryption"
android:title="Retry decryption"
diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml
index ad62138e..bc692996 100644
--- a/src/main/res/values-ru/strings.xml
+++ b/src/main/res/values-ru/strings.xml
@@ -331,6 +331,7 @@
<string name="check_x_filesize_on_host">Проверить размер %1$s на %2$s</string>
<string name="message_options">Опции сообщения</string>
<string name="copy_text">Копировать текст</string>
+ <string name="select_text">Выбрать текст</string>
<string name="copy_original_url">Копировать адрес ссылки</string>
<string name="send_again">Отправить ещё раз</string>
<string name="file_url">URL файла</string>
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 170cbfff..c2f86189 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -363,6 +363,7 @@
<string name="check_x_filesize_on_host">Check %1$s size on %2$s</string>
<string name="message_options">Message options</string>
<string name="copy_text">Copy text</string>
+ <string name="select_text">Select text</string>
<string name="copy_original_url">Copy original URL</string>
<string name="send_again">Send again</string>
<string name="file_url">File URL</string>