context menu for messages. allow to resend single messages

iNPUTmice created

Change summary

res/layout/message_received.xml                                |   4 
res/layout/message_sent.xml                                    |   4 
res/menu/message_context.xml                                   |  17 
res/values/strings.xml                                         |  19 
src/eu/siacs/conversations/entities/Message.java               |  11 
src/eu/siacs/conversations/services/XmppConnectionService.java |  17 
src/eu/siacs/conversations/ui/ConversationActivity.java        |   1 
src/eu/siacs/conversations/ui/ConversationFragment.java        | 101 +++
src/eu/siacs/conversations/ui/EditAccountActivity.java         |  13 
src/eu/siacs/conversations/ui/XmppActivity.java                |  13 
src/eu/siacs/conversations/ui/adapter/MessageAdapter.java      |  28 
11 files changed, 189 insertions(+), 39 deletions(-)

Detailed changes

res/layout/message_received.xml πŸ”—

@@ -15,7 +15,8 @@
         android:layout_alignParentBottom="true"
         android:layout_toRightOf="@+id/message_photo"
         android:background="@drawable/message_border"
-        android:minHeight="48dp" >
+        android:minHeight="48dp"
+        android:longClickable="true">
 
         <LinearLayout
             android:layout_width="wrap_content"
@@ -43,7 +44,6 @@
                 android:layout_height="wrap_content"
                 android:autoLink="web"
                 android:textColor="@color/primarytext"
-                android:textIsSelectable="true"
                 android:textSize="?attr/TextSizeBody" />
 
             <Button

res/layout/message_sent.xml πŸ”—

@@ -15,7 +15,8 @@
         android:layout_alignParentBottom="true"
         android:layout_toLeftOf="@+id/message_photo"
         android:background="@drawable/message_border"
-        android:minHeight="48dp" >
+        android:minHeight="48dp"
+        android:longClickable="true">
 
         <LinearLayout
             android:layout_width="wrap_content"
@@ -43,7 +44,6 @@
                 android:layout_height="wrap_content"
                 android:autoLink="web"
                 android:textColor="@color/primarytext"
-                android:textIsSelectable="true"
                 android:textSize="?attr/TextSizeBody" />
             
              <Button

res/menu/message_context.xml πŸ”—

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <item
+        android:id="@+id/copy_text"
+        android:title="@string/copy_text"/>
+    <item
+        android:id="@+id/share_image"
+        android:title="@string/share_image"/>
+    <item
+        android:id="@+id/copy_url"
+        android:title="@string/copy_original_url" />
+    <item
+        android:id="@+id/send_again"
+        android:title="@string/send_again"/>
+
+</menu>

res/values/strings.xml πŸ”—

@@ -249,10 +249,10 @@
     <string name="pref_force_encryption">Force end-to-end encryption</string>
     <string name="pref_force_encryption_summary">Always send messages encrypted (except for conferences)</string>
     <string name="pref_dont_save_encrypted">Don’t save encrypted messages</string>
-		<string name="pref_dont_save_encrypted_summary">Warning: This could lead to message loss</string>
-		<string name="pref_enable_legacy_ssl">Enable legacy SSL</string>
-		<string name="pref_enable_legacy_ssl_summary">Enables SSLv3 support for legacy servers. Warning: SSLv3 is considered insecure.</string>
-		<string name="pref_expert_options">Expert options</string>
+    <string name="pref_dont_save_encrypted_summary">Warning: This could lead to message loss</string>
+    <string name="pref_enable_legacy_ssl">Enable legacy SSL</string>
+    <string name="pref_enable_legacy_ssl_summary">Enables SSLv3 support for legacy servers. Warning: SSLv3 is considered insecure.</string>
+    <string name="pref_expert_options">Expert options</string>
     <string name="pref_expert_options_summary">Please be careful with these</string>
     <string name="pref_use_larger_font">Increase font size</string>
     <string name="pref_use_larger_font_summary">Use larger font sizes across the entire app</string>
@@ -272,5 +272,14 @@
     <string name="image_file_deleted">The image file has been deleted</string>
     <string name="not_connected_try_again">You are not connected. Try again later</string>
     <string name="check_image_filesize">Check image file size</string>
+    <string name="message_options">Message options</string>
+    <string name="copy_text">Copy text</string>
+    <string name="share_image">Share image</string>
+    <string name="copy_original_url">Copy original URL</string>
+    <string name="send_again">Send again</string>
+    <string name="image_url">Image URL</string>
+    <string name="message_text">Message text</string>
+    <string name="url_copied_to_clipboard">URL copied to clipboard</string>
+    <string name="message_copied_to_clipboard">Message copied to clipboard</string>
 
-</resources>
+</resources>

src/eu/siacs/conversations/entities/Message.java πŸ”—

@@ -431,6 +431,11 @@ public class Message extends AbstractEntity {
 				params.size = Long.parseLong(parts[0]);
 			} catch (NumberFormatException e) {
 				params.origin = parts[0];
+				try {
+					params.url = new URL(parts[0]);
+				} catch (MalformedURLException e1) {
+					params.url = null;
+				}
 			}
 		} else if (parts.length == 3) {
 			try {
@@ -450,6 +455,11 @@ public class Message extends AbstractEntity {
 			}
 		} else if (parts.length == 4) {
 			params.origin = parts[0];
+			try {
+				params.url = new URL(parts[0]);
+			} catch (MalformedURLException e1) {
+				params.url = null;
+			}
 			try {
 				params.size = Long.parseLong(parts[1]);
 			} catch (NumberFormatException e) {
@@ -470,6 +480,7 @@ public class Message extends AbstractEntity {
 	}
 
 	public class ImageParams {
+		public URL url;
 		public long size = 0;
 		public int width = 0;
 		public int height = 0;

src/eu/siacs/conversations/services/XmppConnectionService.java πŸ”—

@@ -1924,4 +1924,21 @@ public class XmppConnectionService extends Service {
 		}
 
 	}
+
+	public void resendFailedMessages(Message message) {
+		List<Message> messages = new ArrayList<Message>();
+		Message current = message;
+		while(current.getStatus() == Message.STATUS_SEND_FAILED) {
+			messages.add(current);
+			if (current.mergable(current.next())) {
+				current = current.next();
+			} else {
+				break;
+			}
+		}
+		for(Message msg: messages) {
+			markMessage(msg, Message.STATUS_WAITING);
+			this.resendMessage(msg);
+		}
+	}
 }

src/eu/siacs/conversations/ui/ConversationFragment.java πŸ”—

@@ -11,6 +11,7 @@ import eu.siacs.conversations.crypto.PgpEngine;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.entities.Presences;
@@ -33,9 +34,12 @@ import android.content.IntentSender.SendIntentException;
 import android.os.Bundle;
 import android.text.Editable;
 import android.text.Selection;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
 import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
+import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
@@ -45,6 +49,8 @@ import android.widget.AbsListView.OnScrollListener;
 import android.widget.TextView.OnEditorActionListener;
 import android.widget.AbsListView;
 
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
 import android.widget.ListView;
 import android.widget.ImageButton;
 import android.widget.RelativeLayout;
@@ -193,6 +199,7 @@ public class ConversationFragment extends Fragment {
 	};
 
 	private ConversationActivity activity;
+	private Message selectedMessage;
 
 	private void sendMessage() {
 		if (this.conversation == null) {
@@ -326,9 +333,100 @@ public class ConversationFragment extends Fragment {
 				});
 		messagesView.setAdapter(messageListAdapter);
 
+		registerForContextMenu(messagesView);
+
 		return view;
 	}
 
+	@Override
+	public void onCreateContextMenu(ContextMenu menu, View v,
+			ContextMenuInfo menuInfo) {
+		super.onCreateContextMenu(menu, v, menuInfo);
+		AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
+		this.selectedMessage = this.messageList.get(acmi.position);
+		populateContextMenu(menu);
+	}
+
+	private void populateContextMenu(ContextMenu menu) {
+		if (this.selectedMessage.getType() != Message.TYPE_STATUS) {
+			activity.getMenuInflater().inflate(R.menu.message_context, menu);
+			menu.setHeaderTitle(R.string.message_options);
+			MenuItem copyText = menu.findItem(R.id.copy_text);
+			MenuItem shareImage = menu.findItem(R.id.share_image);
+			MenuItem sendAgain = menu.findItem(R.id.send_again);
+			MenuItem copyUrl = menu.findItem(R.id.copy_url);
+			if (this.selectedMessage.getType() != Message.TYPE_TEXT
+					|| this.selectedMessage.getDownloadable() != null) {
+				copyText.setVisible(false);
+			}
+			if (this.selectedMessage.getType() != Message.TYPE_IMAGE
+					|| (this.selectedMessage.getDownloadable() != null && this.selectedMessage
+							.getDownloadable().getStatus() == Downloadable.STATUS_DELETED)) {
+				shareImage.setVisible(false);
+			}
+			if (this.selectedMessage.getStatus() != Message.STATUS_SEND_FAILED) {
+				sendAgain.setVisible(false);
+			}
+			if ((this.selectedMessage.getType() != Message.TYPE_IMAGE && this.selectedMessage
+					.getDownloadable() == null)
+					|| this.selectedMessage.getImageParams().url == null) {
+				copyUrl.setVisible(false);
+			}
+		}
+	}
+
+	@Override
+	public boolean onContextItemSelected(MenuItem item) {
+		switch (item.getItemId()) {
+		case R.id.share_image:
+			shareImage(selectedMessage);
+			return true;
+		case R.id.copy_text:
+			copyText(selectedMessage);
+			return true;
+		case R.id.send_again:
+			resendMessage(selectedMessage);
+			return true;
+		case R.id.copy_url:
+			copyUrl(selectedMessage);
+			return true;
+		default:
+			return super.onContextItemSelected(item);
+		}
+	}
+
+	private void shareImage(Message message) {
+		Intent shareIntent = new Intent();
+		shareIntent.setAction(Intent.ACTION_SEND);
+		shareIntent.putExtra(Intent.EXTRA_STREAM,
+				activity.xmppConnectionService.getFileBackend()
+						.getJingleFileUri(message));
+		shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+		shareIntent.setType("image/webp");
+		activity.startActivity(Intent.createChooser(shareIntent,
+				getText(R.string.share_with)));
+	}
+
+	private void copyText(Message message) {
+		if (activity.copyTextToClipboard(message.getMergedBody(),
+				R.string.message_text)) {
+			Toast.makeText(activity, R.string.message_copied_to_clipboard,
+					Toast.LENGTH_SHORT).show();
+		}
+	}
+
+	private void resendMessage(Message message) {
+		activity.xmppConnectionService.resendFailedMessages(message);
+	}
+
+	private void copyUrl(Message message) {
+		if (activity.copyTextToClipboard(
+				message.getImageParams().url.toString(), R.string.image_url)) {
+			Toast.makeText(activity, R.string.url_copied_to_clipboard,
+					Toast.LENGTH_SHORT).show();
+		}
+	}
+
 	protected void privateMessageWith(String counterpart) {
 		this.mEditMessage.setText("");
 		this.conversation.setNextPresence(counterpart);
@@ -437,7 +535,8 @@ public class ConversationFragment extends Fragment {
 			for (Message message : this.conversation.getMessages()) {
 				if (message.getEncryption() == Message.ENCRYPTION_PGP
 						&& (message.getStatus() == Message.STATUS_RECEIVED || message
-								.getStatus() >= Message.STATUS_SEND) && message.getDownloadable() == null) {
+								.getStatus() >= Message.STATUS_SEND)
+						&& message.getDownloadable() == null) {
 					if (!mEncryptedMessages.contains(message)) {
 						mEncryptedMessages.add(message);
 					}

src/eu/siacs/conversations/ui/EditAccountActivity.java πŸ”—

@@ -389,7 +389,7 @@ public class EditAccountActivity extends XmppActivity {
 							@Override
 							public void onClick(View v) {
 
-								if (OtrFingerprintToClipBoard(fingerprint)) {
+								if (copyTextToClipboard(fingerprint,R.string.otr_fingerprint)) {
 									Toast.makeText(
 											EditAccountActivity.this,
 											R.string.toast_message_otr_fingerprint,
@@ -409,15 +409,4 @@ public class EditAccountActivity extends XmppActivity {
 			this.mStats.setVisibility(View.GONE);
 		}
 	}
-
-	private boolean OtrFingerprintToClipBoard(String fingerprint) {
-		ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
-		String label = getResources().getString(R.string.otr_fingerprint);
-		if (mClipBoardManager != null) {
-			ClipData mClipData = ClipData.newPlainText(label, fingerprint);
-			mClipBoardManager.setPrimaryClip(mClipData);
-			return true;
-		}
-		return false;
-	}
 }

src/eu/siacs/conversations/ui/XmppActivity.java πŸ”—

@@ -21,6 +21,8 @@ import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.PendingIntent;
 import android.app.AlertDialog.Builder;
+import android.content.ClipData;
+import android.content.ClipboardManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -531,6 +533,17 @@ public abstract class XmppActivity extends Activity {
 		DisplayMetrics metrics = getResources().getDisplayMetrics();
 		return ((int) (dp * metrics.density));
 	}
+	
+	public boolean copyTextToClipboard(String text,int labelResId) {
+		ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+		String label = getResources().getString(labelResId);
+		if (mClipBoardManager != null) {
+			ClipData mClipData = ClipData.newPlainText(label, text);
+			mClipBoardManager.setPrimaryClip(mClipData);
+			return true;
+		}
+		return false;
+	}
 
 	public AvatarService avatarService() {
 		return xmppConnectionService.getAvatarService();

src/eu/siacs/conversations/ui/adapter/MessageAdapter.java πŸ”—

@@ -43,6 +43,15 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 	private OnContactPictureClicked mOnContactPictureClickedListener;
 	private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
 
+	private OnLongClickListener openContextMenu = new OnLongClickListener() {
+		
+		@Override
+		public boolean onLongClick(View v) {
+			v.showContextMenu();
+			return true;
+		}
+	};
+	
 	public MessageAdapter(ConversationActivity activity, List<Message> messages) {
 		super(activity, 0, messages);
 		this.activity = activity;
@@ -259,6 +268,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 				startDonwloadable(message);
 			}
 		});
+		viewHolder.download_button.setOnLongClickListener(openContextMenu);
 	}
 
 	private void displayImageMessage(ViewHolder viewHolder,
@@ -292,23 +302,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 				getContext().startActivity(intent);
 			}
 		});
-		viewHolder.image.setOnLongClickListener(new OnLongClickListener() {
-
-			@Override
-			public boolean onLongClick(View v) {
-				Intent shareIntent = new Intent();
-				shareIntent.setAction(Intent.ACTION_SEND);
-				shareIntent.putExtra(Intent.EXTRA_STREAM,
-						activity.xmppConnectionService.getFileBackend()
-								.getJingleFileUri(message));
-				shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-				shareIntent.setType("image/webp");
-				getContext().startActivity(
-						Intent.createChooser(shareIntent,
-								getContext().getText(R.string.share_with)));
-				return true;
-			}
-		});
+		viewHolder.image.setOnLongClickListener(openContextMenu);
 	}
 
 	@Override