Merge branch 'feature-selection' of https://github.com/Mishiranu/Conversations into Mishiranu-feature-selection

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java        |  24 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java      |  21 
src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java | 201 
src/main/res/menu/message_context.xml                                    |   4 
src/main/res/values-ru/strings.xml                                       |   1 
src/main/res/values/strings.xml                                          |   1 
6 files changed, 252 insertions(+)

Detailed changes

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

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

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) {
+			}
+		}
+	}
+}

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"

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>

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>