vastly untested refactor. pushing for backup purposes

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java                |   3 
src/main/java/eu/siacs/conversations/ui/ConversationActivity.java              | 801 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java              | 848 
src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java                 |   5 
src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java                 |   2 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                      | 141 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java            |   3 
src/main/java/eu/siacs/conversations/ui/util/ActivityResult.java               |  50 
src/main/java/eu/siacs/conversations/ui/util/AttachmentTool.java               |  61 
src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java | 123 
src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java             | 136 
src/main/java/eu/siacs/conversations/ui/util/SendButtonAction.java             |  10 
src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java               | 188 
src/main/java/eu/siacs/conversations/utils/UIHelper.java                       |  17 
src/main/res/menu/activity_conversations.xml                                   |  22 
src/main/res/menu/fragment_conversation.xml                                    |  22 
16 files changed, 1,370 insertions(+), 1,062 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java 🔗

@@ -18,8 +18,7 @@ import eu.siacs.conversations.entities.Blockable;
 import eu.siacs.conversations.entities.Conversation;
 
 public final class BlockContactDialog {
-	public static void show(final XmppActivity xmppActivity,
-			final Blockable blockable) {
+	public static void show(final XmppActivity xmppActivity, final Blockable blockable) {
 		final AlertDialog.Builder builder = new AlertDialog.Builder(xmppActivity);
 		final boolean isBlocked = blockable.isBlocked();
 		builder.setNegativeButton(R.string.cancel, null);

src/main/java/eu/siacs/conversations/ui/ConversationActivity.java 🔗

@@ -6,23 +6,17 @@ import android.app.FragmentTransaction;
 import android.app.PendingIntent;
 import android.content.ActivityNotFoundException;
 import android.content.ClipData;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.IntentSender.SendIntentException;
-import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
-import android.provider.MediaStore;
 import android.provider.Settings;
 import android.support.v4.widget.SlidingPaneLayout;
 import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
 import android.support.v7.app.ActionBar;
 import android.util.Log;
 import android.util.Pair;
-import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -31,10 +25,8 @@ import android.view.View;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ArrayAdapter;
-import android.widget.CheckBox;
 import android.widget.Toast;
 
-import org.openintents.openpgp.util.OpenPgpApi;
 
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -48,18 +40,14 @@ import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Blockable;
-import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.entities.Transferable;
-import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
 import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
 import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
 import eu.siacs.conversations.ui.adapter.ConversationAdapter;
 import eu.siacs.conversations.ui.service.EmojiService;
-import eu.siacs.conversations.ui.util.SendButtonAction;
 import eu.siacs.conversations.utils.ExceptionHelper;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 import eu.siacs.conversations.xmpp.XmppConnection;
@@ -69,8 +57,6 @@ import eu.siacs.conversations.xmpp.jid.Jid;
 public class ConversationActivity extends XmppActivity
 		implements OnAccountUpdate, OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast {
 
-	public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action";
-
 	public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";
 	public static final String CONVERSATION = "conversationUuid";
 	public static final String EXTRA_DOWNLOAD_UUID = "eu.siacs.conversations.download_uuid";
@@ -78,20 +64,6 @@ public class ConversationActivity extends XmppActivity
 	public static final String NICK = "nick";
 	public static final String PRIVATE_MESSAGE = "pm";
 
-	public static final int REQUEST_SEND_MESSAGE = 0x0201;
-	public static final int REQUEST_DECRYPT_PGP = 0x0202;
-	public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
-	public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208;
-	public static final int REQUEST_TRUST_KEYS_MENU = 0x0209;
-	public static final int REQUEST_START_DOWNLOAD = 0x0210;
-	public static final int REQUEST_ADD_EDITOR_CONTENT = 0x0211;
-	public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
-	public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
-	public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303;
-	public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304;
-	public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305;
-	public static final int ATTACHMENT_CHOICE_INVALID = 0x0306;
-	public static final int ATTACHMENT_CHOICE_RECORD_VIDEO = 0x0307;
 	private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
 	private static final String STATE_PANEL_OPEN = "state_panel_open";
 	private static final String STATE_PENDING_URI = "state_pending_uri";
@@ -102,17 +74,10 @@ public class ConversationActivity extends XmppActivity
 	private boolean mPanelOpen = true;
 	private AtomicBoolean mShouldPanelBeOpen = new AtomicBoolean(false);
 	private Pair<Integer, Integer> mScrollPosition = null;
-	final private List<Uri> mPendingImageUris = new ArrayList<>();
-	final private List<Uri> mPendingFileUris = new ArrayList<>();
-	private Uri mPendingGeoUri = null;
 	private boolean forbidProcessingPendings = false;
-	private Message mPendingDownloadableMessage = null;
 
 	private boolean conversationWasSelectedByKeyboard = false;
 
-	private boolean showSoundRecorderAttachment = false;
-	private boolean showLocationAttachment = false;
-
 	private View mContentView;
 
 	private List<Conversation> conversationList = new ArrayList<>();
@@ -125,9 +90,7 @@ public class ConversationActivity extends XmppActivity
 
 	private boolean mActivityPaused = false;
 	private AtomicBoolean mRedirected = new AtomicBoolean(false);
-	private Pair<Integer, Intent> mPostponedActivityResult;
 	private boolean mUnprocessedNewIntent = false;
-	public Uri mPendingEditorContent = null;
 
 	public Conversation getSelectedConversation() {
 		return this.mSelectedConversation;
@@ -193,12 +156,6 @@ public class ConversationActivity extends XmppActivity
 			} else {
 				mScrollPosition = null;
 			}
-			String pending = savedInstanceState.getString(STATE_PENDING_URI, null);
-			if (pending != null) {
-				Log.d(Config.LOGTAG, "ConversationsActivity.onCreate() - restoring pending image uri");
-				mPendingImageUris.clear();
-				mPendingImageUris.add(Uri.parse(pending));
-			}
 		}
 
 		setContentView(R.layout.fragment_conversations_overview);
@@ -395,271 +352,10 @@ public class ConversationActivity extends XmppActivity
 
 	@Override
 	public boolean onCreateOptionsMenu(Menu menu) {
-		getMenuInflater().inflate(R.menu.conversations, menu);
-		final MenuItem menuSecure = menu.findItem(R.id.action_security);
-		final MenuItem menuArchive = menu.findItem(R.id.action_archive);
-		final MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details);
-		final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details);
-		final MenuItem menuAttach = menu.findItem(R.id.action_attach_file);
-		final MenuItem menuClearHistory = menu.findItem(R.id.action_clear_history);
-		final MenuItem menuAdd = menu.findItem(R.id.action_add);
-		final MenuItem menuInviteContact = menu.findItem(R.id.action_invite);
-		final MenuItem menuMute = menu.findItem(R.id.action_mute);
-		final MenuItem menuUnmute = menu.findItem(R.id.action_unmute);
-		final MenuItem menuAttachSoundRecorder = menu.findItem(R.id.attach_record_voice);
-		final MenuItem menuAttachLocation = menu.findItem(R.id.attach_location);
-
-		if (isConversationsOverviewVisable() && isConversationsOverviewHideable()) {
-			menuArchive.setVisible(false);
-			menuMucDetails.setVisible(false);
-			menuContactDetails.setVisible(false);
-			menuSecure.setVisible(false);
-			menuInviteContact.setVisible(false);
-			menuAttach.setVisible(false);
-			menuClearHistory.setVisible(false);
-			menuMute.setVisible(false);
-			menuUnmute.setVisible(false);
-		} else {
-			menuAdd.setVisible(!isConversationsOverviewHideable());
-			if (this.getSelectedConversation() != null) {
-				if (this.getSelectedConversation().getNextEncryption() != Message.ENCRYPTION_NONE) {
-					menuSecure.setIcon(R.drawable.ic_lock_white_24dp);
-				}
-				if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) {
-					menuContactDetails.setVisible(false);
-					menuAttach.setVisible(getSelectedConversation().getAccount().httpUploadAvailable() && getSelectedConversation().getMucOptions().participating());
-					menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite());
-					menuSecure.setVisible((Config.supportOpenPgp() || Config.supportOmemo()) && Config.multipleEncryptionChoices()); //only if pgp is supported we have a choice
-				} else {
-					menuContactDetails.setVisible(!this.getSelectedConversation().withSelf());
-					menuMucDetails.setVisible(false);
-					menuSecure.setVisible(Config.multipleEncryptionChoices());
-					menuInviteContact.setVisible(xmppConnectionService != null && xmppConnectionService.findConferenceServer(getSelectedConversation().getAccount()) != null);
-				}
-				if (this.getSelectedConversation().isMuted()) {
-					menuMute.setVisible(false);
-				} else {
-					menuUnmute.setVisible(false);
-				}
-				menuAttachLocation.setVisible(showLocationAttachment);
-				menuAttachSoundRecorder.setVisible(showSoundRecorderAttachment);
-				configureEncryptionMenu(getSelectedConversation(), menu);
-			}
-		}
+		getMenuInflater().inflate(R.menu.activity_conversations, menu);
 		return super.onCreateOptionsMenu(menu);
 	}
 
-	private static void configureEncryptionMenu(Conversation conversation, Menu menu) {
-		MenuItem none = menu.findItem(R.id.encryption_choice_none);
-		MenuItem pgp = menu.findItem(R.id.encryption_choice_pgp);
-		MenuItem axolotl = menu.findItem(R.id.encryption_choice_axolotl);
-		pgp.setVisible(Config.supportOpenPgp());
-		none.setVisible(Config.supportUnencrypted() || conversation.getMode() == Conversation.MODE_MULTI);
-		axolotl.setVisible(Config.supportOmemo());
-		final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
-		if (axolotlService == null || !axolotlService.isConversationAxolotlCapable(conversation)) {
-			axolotl.setEnabled(false);
-		}
-		switch (conversation.getNextEncryption()) {
-			case Message.ENCRYPTION_NONE:
-				none.setChecked(true);
-				break;
-			case Message.ENCRYPTION_PGP:
-				pgp.setChecked(true);
-				break;
-			case Message.ENCRYPTION_AXOLOTL:
-				axolotl.setChecked(true);
-				break;
-			default:
-				none.setChecked(true);
-				break;
-		}
-	}
-
-	protected void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) {
-		final Conversation conversation = getSelectedConversation();
-		final Account account = conversation.getAccount();
-		final OnPresenceSelected callback = () -> {
-			Intent intent = new Intent();
-			boolean chooser = false;
-			String fallbackPackageId = null;
-			switch (attachmentChoice) {
-				case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
-					intent.setAction(Intent.ACTION_GET_CONTENT);
-					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
-						intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
-					}
-					intent.setType("image/*");
-					chooser = true;
-					break;
-				case ATTACHMENT_CHOICE_RECORD_VIDEO:
-					intent.setAction(MediaStore.ACTION_VIDEO_CAPTURE);
-					break;
-				case ATTACHMENT_CHOICE_TAKE_PHOTO:
-					Uri uri = xmppConnectionService.getFileBackend().getTakePhotoUri();
-					mPendingImageUris.clear();
-					mPendingImageUris.add(uri);
-					intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
-					intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
-					intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-					intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
-					break;
-				case ATTACHMENT_CHOICE_CHOOSE_FILE:
-					chooser = true;
-					intent.setType("*/*");
-					intent.addCategory(Intent.CATEGORY_OPENABLE);
-					intent.setAction(Intent.ACTION_GET_CONTENT);
-					break;
-				case ATTACHMENT_CHOICE_RECORD_VOICE:
-					intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
-					fallbackPackageId = "eu.siacs.conversations.voicerecorder";
-					break;
-				case ATTACHMENT_CHOICE_LOCATION:
-					intent.setAction("eu.siacs.conversations.location.request");
-					fallbackPackageId = "eu.siacs.conversations.sharelocation";
-					break;
-			}
-			if (intent.resolveActivity(getPackageManager()) != null) {
-				if (chooser) {
-					startActivityForResult(
-							Intent.createChooser(intent, getString(R.string.perform_action_with)),
-							attachmentChoice);
-				} else {
-					startActivityForResult(intent, attachmentChoice);
-				}
-			} else if (fallbackPackageId != null) {
-				startActivity(getInstallApkIntent(fallbackPackageId));
-			}
-		};
-		if (account.httpUploadAvailable() || attachmentChoice == ATTACHMENT_CHOICE_LOCATION) {
-			conversation.setNextCounterpart(null);
-			callback.onPresenceSelected();
-		} else {
-			selectPresence(conversation, callback);
-		}
-	}
-
-	private Intent getInstallApkIntent(final String packageId) {
-		Intent intent = new Intent(Intent.ACTION_VIEW);
-		intent.setData(Uri.parse("market://details?id=" + packageId));
-		if (intent.resolveActivity(getPackageManager()) != null) {
-			return intent;
-		} else {
-			intent.setData(Uri.parse("http://play.google.com/store/apps/details?id=" + packageId));
-			return intent;
-		}
-	}
-
-	public void attachFile(final int attachmentChoice) {
-		if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) {
-			if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(attachmentChoice)) {
-				return;
-			}
-		}
-		try {
-			getPreferences().edit()
-					.putString(RECENTLY_USED_QUICK_ACTION, SendButtonAction.of(attachmentChoice).toString())
-					.apply();
-		} catch (IllegalArgumentException e) {
-			//just do not save
-		}
-		final Conversation conversation = getSelectedConversation();
-		final int encryption = conversation.getNextEncryption();
-		final int mode = conversation.getMode();
-		if (encryption == Message.ENCRYPTION_PGP) {
-			if (hasPgp()) {
-				if (mode == Conversation.MODE_SINGLE && conversation.getContact().getPgpKeyId() != 0) {
-					xmppConnectionService.getPgpEngine().hasKey(
-							conversation.getContact(),
-							new UiCallback<Contact>() {
-
-								@Override
-								public void userInputRequried(PendingIntent pi, Contact contact) {
-									ConversationActivity.this.runIntent(pi, attachmentChoice);
-								}
-
-								@Override
-								public void success(Contact contact) {
-									selectPresenceToAttachFile(attachmentChoice, encryption);
-								}
-
-								@Override
-								public void error(int error, Contact contact) {
-									replaceToast(getString(error));
-								}
-							});
-				} else if (mode == Conversation.MODE_MULTI && conversation.getMucOptions().pgpKeysInUse()) {
-					if (!conversation.getMucOptions().everybodyHasKeys()) {
-						Toast warning = Toast
-								.makeText(this,
-										R.string.missing_public_keys,
-										Toast.LENGTH_LONG);
-						warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
-						warning.show();
-					}
-					selectPresenceToAttachFile(attachmentChoice, encryption);
-				} else {
-					final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
-							.findFragmentByTag("conversation");
-					if (fragment != null) {
-						fragment.showNoPGPKeyDialog(false,
-								new OnClickListener() {
-
-									@Override
-									public void onClick(DialogInterface dialog,
-									                    int which) {
-										conversation.setNextEncryption(Message.ENCRYPTION_NONE);
-										xmppConnectionService.updateConversation(conversation);
-										selectPresenceToAttachFile(attachmentChoice, Message.ENCRYPTION_NONE);
-									}
-								});
-					}
-				}
-			} else {
-				showInstallPgpDialog();
-			}
-		} else {
-			if (encryption != Message.ENCRYPTION_AXOLOTL || !trustKeysIfNeeded(REQUEST_TRUST_KEYS_MENU, attachmentChoice)) {
-				selectPresenceToAttachFile(attachmentChoice, encryption);
-			}
-		}
-	}
-
-	@Override
-	public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
-		if (grantResults.length > 0)
-			if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
-				if (requestCode == REQUEST_START_DOWNLOAD) {
-					if (this.mPendingDownloadableMessage != null) {
-						startDownloadable(this.mPendingDownloadableMessage);
-					}
-				} else if (requestCode == REQUEST_ADD_EDITOR_CONTENT) {
-					if (this.mPendingEditorContent != null) {
-						attachImageToConversation(this.mPendingEditorContent);
-					}
-				} else {
-					attachFile(requestCode);
-				}
-			} else {
-				Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
-			}
-	}
-
-	public void startDownloadable(Message message) {
-		if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(ConversationActivity.REQUEST_START_DOWNLOAD)) {
-			this.mPendingDownloadableMessage = message;
-			return;
-		}
-		Transferable transferable = message.getTransferable();
-		if (transferable != null) {
-			if (!transferable.start()) {
-				Toast.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
-			}
-		} else if (message.treatAsDownloadable()) {
-			xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true);
-		}
-	}
-
 	@Override
 	public boolean onOptionsItemSelected(final MenuItem item) {
 		if (item.getItemId() == android.R.id.home) {
@@ -668,56 +364,6 @@ public class ConversationActivity extends XmppActivity
 		} else if (item.getItemId() == R.id.action_add) {
 			startActivity(new Intent(this, StartConversationActivity.class));
 			return true;
-		} else if (getSelectedConversation() != null) {
-			switch (item.getItemId()) {
-				case R.id.encryption_choice_axolotl:
-				case R.id.encryption_choice_pgp:
-				case R.id.encryption_choice_none:
-					handleEncryptionSelection(item);
-					break;
-				case R.id.attach_choose_picture:
-				case R.id.attach_take_picture:
-				case R.id.attach_record_video:
-				case R.id.attach_choose_file:
-				case R.id.attach_record_voice:
-				case R.id.attach_location:
-					handleAttachmentSelection(item);
-					break;
-				case R.id.action_archive:
-					this.endConversation(getSelectedConversation());
-					break;
-				case R.id.action_contact_details:
-					switchToContactDetails(getSelectedConversation().getContact());
-					break;
-				case R.id.action_muc_details:
-					Intent intent = new Intent(this,
-							ConferenceDetailsActivity.class);
-					intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
-					intent.putExtra("uuid", getSelectedConversation().getUuid());
-					startActivity(intent);
-					break;
-				case R.id.action_invite:
-					inviteToConversation(getSelectedConversation());
-					break;
-				case R.id.action_clear_history:
-					clearHistoryDialog(getSelectedConversation());
-					break;
-				case R.id.action_mute:
-					muteConversationDialog(getSelectedConversation());
-					break;
-				case R.id.action_unmute:
-					unmuteConversation(getSelectedConversation());
-					break;
-				case R.id.action_block:
-					BlockContactDialog.show(this, getSelectedConversation());
-					break;
-				case R.id.action_unblock:
-					BlockContactDialog.show(this, getSelectedConversation());
-					break;
-				default:
-					break;
-			}
-			return super.onOptionsItemSelected(item);
 		} else {
 			return super.onOptionsItemSelected(item);
 		}
@@ -748,116 +394,6 @@ public class ConversationActivity extends XmppActivity
 		}
 	}
 
-	@SuppressLint("InflateParams")
-	protected void clearHistoryDialog(final Conversation conversation) {
-		AlertDialog.Builder builder = new AlertDialog.Builder(this);
-		builder.setTitle(getString(R.string.clear_conversation_history));
-		final View dialogView = getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
-		final CheckBox endConversationCheckBox = dialogView.findViewById(R.id.end_conversation_checkbox);
-		builder.setView(dialogView);
-		builder.setNegativeButton(getString(R.string.cancel), null);
-		builder.setPositiveButton(getString(R.string.delete_messages), (dialog, which) -> {
-			ConversationActivity.this.xmppConnectionService.clearConversationHistory(conversation);
-			if (endConversationCheckBox.isChecked()) {
-				endConversation(conversation);
-			} else {
-				updateConversationList();
-				ConversationActivity.this.mConversationFragment.updateMessages();
-			}
-		});
-		builder.create().show();
-	}
-
-	private void handleAttachmentSelection(MenuItem item) {
-		switch (item.getItemId()) {
-			case R.id.attach_choose_picture:
-				attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
-				break;
-			case R.id.attach_take_picture:
-				attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
-				break;
-			case R.id.attach_record_video:
-				attachFile(ATTACHMENT_CHOICE_RECORD_VIDEO);
-				break;
-			case R.id.attach_choose_file:
-				attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE);
-				break;
-			case R.id.attach_record_voice:
-				attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
-				break;
-			case R.id.attach_location:
-				attachFile(ATTACHMENT_CHOICE_LOCATION);
-				break;
-		}
-	}
-
-	private void handleEncryptionSelection(MenuItem item) {
-		Conversation conversation = getSelectedConversation();
-		if (conversation == null) {
-			return;
-		}
-		final ConversationFragment fragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation");
-		switch (item.getItemId()) {
-			case R.id.encryption_choice_none:
-				conversation.setNextEncryption(Message.ENCRYPTION_NONE);
-				item.setChecked(true);
-				break;
-			case R.id.encryption_choice_pgp:
-				if (hasPgp()) {
-					if (conversation.getAccount().getPgpSignature() != null) {
-						conversation.setNextEncryption(Message.ENCRYPTION_PGP);
-						item.setChecked(true);
-					} else {
-						announcePgp(conversation.getAccount(), conversation, null, onOpenPGPKeyPublished);
-					}
-				} else {
-					showInstallPgpDialog();
-				}
-				break;
-			case R.id.encryption_choice_axolotl:
-				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount())
-						+ "Enabled axolotl for Contact " + conversation.getContact().getJid());
-				conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL);
-				item.setChecked(true);
-				break;
-			default:
-				conversation.setNextEncryption(Message.ENCRYPTION_NONE);
-				break;
-		}
-		xmppConnectionService.updateConversation(conversation);
-		fragment.updateChatMsgHint();
-		invalidateOptionsMenu();
-		refreshUi();
-	}
-
-	protected void muteConversationDialog(final Conversation conversation) {
-		AlertDialog.Builder builder = new AlertDialog.Builder(this);
-		builder.setTitle(R.string.disable_notifications);
-		final int[] durations = getResources().getIntArray(R.array.mute_options_durations);
-		builder.setItems(R.array.mute_options_descriptions, (dialog, which) -> {
-			final long till;
-			if (durations[which] == -1) {
-				till = Long.MAX_VALUE;
-			} else {
-				till = System.currentTimeMillis() + (durations[which] * 1000);
-			}
-			conversation.setMutedTill(till);
-			ConversationActivity.this.xmppConnectionService.updateConversation(conversation);
-			updateConversationList();
-			ConversationActivity.this.mConversationFragment.updateMessages();
-			invalidateOptionsMenu();
-		});
-		builder.create().show();
-	}
-
-	public void unmuteConversation(final Conversation conversation) {
-		conversation.setMutedTill(0);
-		this.xmppConnectionService.updateConversation(conversation);
-		updateConversationList();
-		ConversationActivity.this.mConversationFragment.updateMessages();
-		invalidateOptionsMenu();
-	}
-
 	@Override
 	public void onBackPressed() {
 		if (!isConversationsOverviewVisable()) {
@@ -1025,11 +561,6 @@ public class ConversationActivity extends XmppActivity
 		if (!isConversationsOverviewVisable() || !isConversationsOverviewHideable()) {
 			sendReadMarkerIfNecessary(getSelectedConversation());
 		}
-		new Handler().post(() -> {
-			showSoundRecorderAttachment = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(getPackageManager()) != null;
-			showLocationAttachment = new Intent("eu.siacs.conversations.location.request").resolveActivity(getPackageManager()) != null;
-			invalidateOptionsMenu();
-		});
 	}
 
 	@Override
@@ -1046,20 +577,17 @@ public class ConversationActivity extends XmppActivity
 			savedInstanceState.remove(STATE_OPEN_CONVERSATION);
 		}
 		savedInstanceState.putBoolean(STATE_PANEL_OPEN, isConversationsOverviewVisable());
-		if (this.mPendingImageUris.size() >= 1) {
+		/*if (this.mPendingImageUris.size() >= 1) {
 			Log.d(Config.LOGTAG, "ConversationsActivity.onSaveInstanceState() - saving pending image uri");
 			savedInstanceState.putString(STATE_PENDING_URI, this.mPendingImageUris.get(0).toString());
 		} else {
 			savedInstanceState.remove(STATE_PENDING_URI);
-		}
+		}*/
 		super.onSaveInstanceState(savedInstanceState);
 	}
 
 	private void clearPending() {
-		mPendingImageUris.clear();
-		mPendingFileUris.clear();
-		mPendingGeoUri = null;
-		mPostponedActivityResult = null;
+		mConversationFragment.clearPending();
 	}
 
 	private void redirectToStartConversationActivity(boolean noAnimation) {
@@ -1142,30 +670,7 @@ public class ConversationActivity extends XmppActivity
 			this.mConversationFragment.setupIme();
 		}
 
-		if (this.mPostponedActivityResult != null) {
-			this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
-		}
-
-		final boolean stopping = isStopping();
-
-		if (!forbidProcessingPendings) {
-			for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
-				Uri foo = i.next();
-				Log.d(Config.LOGTAG, "ConversationsActivity.onBackendConnected() - attaching image to conversations. stopping=" + Boolean.toString(stopping));
-				attachImageToConversation(getSelectedConversation(), foo);
-			}
-
-			for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
-				Log.d(Config.LOGTAG, "ConversationsActivity.onBackendConnected() - attaching file to conversations. stopping=" + Boolean.toString(stopping));
-				attachFileToConversation(getSelectedConversation(), i.next());
-			}
-
-			if (mPendingGeoUri != null) {
-				attachLocationToConversation(getSelectedConversation(), mPendingGeoUri);
-				mPendingGeoUri = null;
-			}
-		}
-		forbidProcessingPendings = false;
+		mConversationFragment.onBackendConnected();
 
 		if (!ExceptionHelper.checkForCrash(this, this.xmppConnectionService) && !mRedirected.get()) {
 			openBatteryOptimizationDialogIfNeeded();
@@ -1225,7 +730,7 @@ public class ConversationActivity extends XmppActivity
 			if (downloadUuid != null) {
 				final Message message = mSelectedConversation.findMessageWithFileAndUuid(downloadUuid);
 				if (message != null) {
-					startDownloadable(message);
+					//startDownloadable(message);
 				}
 			}
 		} else {
@@ -1252,134 +757,17 @@ public class ConversationActivity extends XmppActivity
 		xmppConnectionService.getNotificationService().setOpenConversation(null);
 	}
 
-	@SuppressLint("NewApi")
-	private static List<Uri> extractUriFromIntent(final Intent intent) {
-		List<Uri> uris = new ArrayList<>();
-		if (intent == null) {
-			return uris;
-		}
-		Uri uri = intent.getData();
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && uri == null) {
-			final ClipData clipData = intent.getClipData();
-			if (clipData != null) {
-				for (int i = 0; i < clipData.getItemCount(); ++i) {
-					uris.add(clipData.getItemAt(i).getUri());
-				}
-			}
-		} else {
-			uris.add(uri);
-		}
-		return uris;
-	}
-
 	@Override
 	protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
 		super.onActivityResult(requestCode, resultCode, data);
-		if (resultCode == RESULT_OK) {
-			if (requestCode == REQUEST_DECRYPT_PGP) {
-				mConversationFragment.onActivityResult(requestCode, resultCode, data);
-			} else if (requestCode == REQUEST_CHOOSE_PGP_ID) {
-				// the user chose OpenPGP for encryption and selected his key in the PGP provider
-				if (xmppConnectionServiceBound) {
-					if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
-						// associate selected PGP keyId with the account
-						mSelectedConversation.getAccount().setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
-						// we need to announce the key as described in XEP-027
-						announcePgp(mSelectedConversation.getAccount(), null, null, onOpenPGPKeyPublished);
-					} else {
-						choosePgpSignId(mSelectedConversation.getAccount());
-					}
-					this.mPostponedActivityResult = null;
-				} else {
-					this.mPostponedActivityResult = new Pair<>(requestCode, data);
-				}
-			} else if (requestCode == REQUEST_ANNOUNCE_PGP) {
-				if (xmppConnectionServiceBound) {
-					announcePgp(mSelectedConversation.getAccount(), mSelectedConversation, data, onOpenPGPKeyPublished);
-					this.mPostponedActivityResult = null;
-				} else {
-					this.mPostponedActivityResult = new Pair<>(requestCode, data);
-				}
-			} else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
-				mPendingImageUris.clear();
-				mPendingImageUris.addAll(extractUriFromIntent(data));
-				if (xmppConnectionServiceBound) {
-					for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
-						Log.d(Config.LOGTAG, "ConversationsActivity.onActivityResult() - attaching image to conversations. CHOOSE_IMAGE");
-						attachImageToConversation(getSelectedConversation(), i.next());
-					}
-				}
-			} else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_FILE || requestCode == ATTACHMENT_CHOICE_RECORD_VOICE || requestCode == ATTACHMENT_CHOICE_RECORD_VIDEO) {
-				final List<Uri> uris = extractUriFromIntent(data);
-				Log.d(Config.LOGTAG, "uris " + uris.toString());
-				final Conversation c = getSelectedConversation();
-				final OnPresenceSelected callback = new OnPresenceSelected() {
-					@Override
-					public void onPresenceSelected() {
-						mPendingFileUris.clear();
-						mPendingFileUris.addAll(uris);
-						if (xmppConnectionServiceBound) {
-							for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
-								Log.d(Config.LOGTAG, "ConversationsActivity.onActivityResult() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
-								attachFileToConversation(c, i.next());
-							}
-						}
-					}
-				};
-				if (c == null || c.getMode() == Conversation.MODE_MULTI
-						|| FileBackend.allFilesUnderSize(this, uris, getMaxHttpUploadSize(c))) {
-					callback.onPresenceSelected();
-				} else {
-					selectPresence(c, callback);
-				}
-			} else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) {
-				if (mPendingImageUris.size() == 1) {
-					Uri uri = FileBackend.getIndexableTakePhotoUri(mPendingImageUris.get(0));
-					mPendingImageUris.set(0, uri);
-					if (xmppConnectionServiceBound) {
-						Log.d(Config.LOGTAG, "ConversationsActivity.onActivityResult() - attaching image to conversations. TAKE_PHOTO");
-						attachImageToConversation(getSelectedConversation(), uri);
-						mPendingImageUris.clear();
-					}
-					if (!Config.ONLY_INTERNAL_STORAGE) {
-						Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
-						intent.setData(uri);
-						sendBroadcast(intent);
-					}
-				} else {
-					mPendingImageUris.clear();
-				}
-			} else if (requestCode == ATTACHMENT_CHOICE_LOCATION) {
-				double latitude = data.getDoubleExtra("latitude", 0);
-				double longitude = data.getDoubleExtra("longitude", 0);
-				this.mPendingGeoUri = Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude));
-				if (xmppConnectionServiceBound) {
-					attachLocationToConversation(getSelectedConversation(), mPendingGeoUri);
-					this.mPendingGeoUri = null;
-				}
-			} else if (requestCode == REQUEST_TRUST_KEYS_TEXT || requestCode == REQUEST_TRUST_KEYS_MENU) {
-				this.forbidProcessingPendings = true;
-				if (xmppConnectionServiceBound) {
-					mConversationFragment.onActivityResult(requestCode, resultCode, data);
-					this.mPostponedActivityResult = null;
-				} else {
-					this.mPostponedActivityResult = new Pair<>(requestCode, data);
-				}
-
-			}
-		} else {
-			mPendingImageUris.clear();
-			mPendingFileUris.clear();
-			if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
-				mConversationFragment.onActivityResult(requestCode, resultCode, data);
-			}
+		if (resultCode != RESULT_OK) {
 			if (requestCode == REQUEST_BATTERY_OP) {
 				setNeverAskForBatteryOptimizationsAgain();
 			}
 		}
 	}
 
-	private long getMaxHttpUploadSize(Conversation conversation) {
+	public long getMaxHttpUploadSize(Conversation conversation) {
 		final XmppConnection connection = conversation.getAccount().getXmppConnection();
 		return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize();
 	}
@@ -1428,108 +816,6 @@ public class ConversationActivity extends XmppActivity
 		return false;
 	}
 
-	private void attachLocationToConversation(Conversation conversation, Uri uri) {
-		if (conversation == null) {
-			return;
-		}
-		xmppConnectionService.attachLocationToConversation(conversation, uri, new UiCallback<Message>() {
-
-			@Override
-			public void success(Message message) {
-				xmppConnectionService.sendMessage(message);
-			}
-
-			@Override
-			public void error(int errorCode, Message object) {
-
-			}
-
-			@Override
-			public void userInputRequried(PendingIntent pi, Message object) {
-
-			}
-		});
-	}
-
-	private void attachFileToConversation(Conversation conversation, Uri uri) {
-		if (conversation == null) {
-			return;
-		}
-		final Toast prepareFileToast = Toast.makeText(getApplicationContext(), getText(R.string.preparing_file), Toast.LENGTH_LONG);
-		prepareFileToast.show();
-		delegateUriPermissionsToService(uri);
-		xmppConnectionService.attachFileToConversation(conversation, uri, new UiInformableCallback<Message>() {
-			@Override
-			public void inform(final String text) {
-				hidePrepareFileToast(prepareFileToast);
-				runOnUiThread(() -> replaceToast(text));
-			}
-
-			@Override
-			public void success(Message message) {
-				runOnUiThread(() -> hideToast());
-				hidePrepareFileToast(prepareFileToast);
-				xmppConnectionService.sendMessage(message);
-			}
-
-			@Override
-			public void error(final int errorCode, Message message) {
-				hidePrepareFileToast(prepareFileToast);
-				runOnUiThread(() -> replaceToast(getString(errorCode)));
-
-			}
-
-			@Override
-			public void userInputRequried(PendingIntent pi, Message message) {
-				hidePrepareFileToast(prepareFileToast);
-			}
-		});
-	}
-
-	public void attachImageToConversation(Uri uri) {
-		this.attachImageToConversation(getSelectedConversation(), uri);
-	}
-
-	private void attachImageToConversation(Conversation conversation, Uri uri) {
-		if (conversation == null) {
-			return;
-		}
-		final Toast prepareFileToast = Toast.makeText(getApplicationContext(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
-		prepareFileToast.show();
-		delegateUriPermissionsToService(uri);
-		xmppConnectionService.attachImageToConversation(conversation, uri,
-				new UiCallback<Message>() {
-
-					@Override
-					public void userInputRequried(PendingIntent pi, Message object) {
-						hidePrepareFileToast(prepareFileToast);
-					}
-
-					@Override
-					public void success(Message message) {
-						hidePrepareFileToast(prepareFileToast);
-						xmppConnectionService.sendMessage(message);
-					}
-
-					@Override
-					public void error(final int error, Message message) {
-						hidePrepareFileToast(prepareFileToast);
-						runOnUiThread(new Runnable() {
-							@Override
-							public void run() {
-								replaceToast(getString(error));
-							}
-						});
-					}
-				});
-	}
-
-	private void hidePrepareFileToast(final Toast prepareFileToast) {
-		if (prepareFileToast != null) {
-			runOnUiThread(() -> prepareFileToast.cancel());
-		}
-	}
-
 	public void updateConversationList() {
 		xmppConnectionService.populateWithOrderedConversations(conversationList);
 		if (!conversationList.contains(mSelectedConversation)) {
@@ -1553,44 +839,6 @@ public class ConversationActivity extends XmppActivity
 		}
 	}
 
-	public void encryptTextMessage(Message message) {
-		xmppConnectionService.getPgpEngine().encrypt(message,
-				new UiCallback<Message>() {
-
-					@Override
-					public void userInputRequried(PendingIntent pi, Message message) {
-						ConversationActivity.this.runIntent(pi, ConversationActivity.REQUEST_SEND_MESSAGE);
-					}
-
-					@Override
-					public void success(Message message) {
-						message.setEncryption(Message.ENCRYPTION_DECRYPTED);
-						xmppConnectionService.sendMessage(message);
-						runOnUiThread(new Runnable() {
-							@Override
-							public void run() {
-								mConversationFragment.messageSent();
-							}
-						});
-					}
-
-					@Override
-					public void error(final int error, Message message) {
-						runOnUiThread(new Runnable() {
-							@Override
-							public void run() {
-								mConversationFragment.doneSendingPgpMessage();
-								Toast.makeText(ConversationActivity.this,
-										R.string.unable_to_connect_to_keychain,
-										Toast.LENGTH_SHORT
-								).show();
-							}
-						});
-
-					}
-				});
-	}
-
 	public boolean useSendButtonToIndicateStatus() {
 		return getPreferences().getBoolean("send_button_status", getResources().getBoolean(R.bool.send_button_status));
 	}
@@ -1603,36 +851,6 @@ public class ConversationActivity extends XmppActivity
 		return getPreferences().getBoolean("use_green_background", getResources().getBoolean(R.bool.use_green_background));
 	}
 
-	protected boolean trustKeysIfNeeded(int requestCode) {
-		return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID);
-	}
-
-	protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
-		AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
-		final List<Jid> targets = axolotlService.getCryptoTargets(mSelectedConversation);
-		boolean hasUnaccepted = !mSelectedConversation.getAcceptedCryptoTargets().containsAll(targets);
-		boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty();
-		boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty();
-		boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
-		boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
-		if (hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) {
-			axolotlService.createSessionsIfNeeded(mSelectedConversation);
-			Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class);
-			String[] contacts = new String[targets.size()];
-			for (int i = 0; i < contacts.length; ++i) {
-				contacts[i] = targets.get(i).toString();
-			}
-			intent.putExtra("contacts", contacts);
-			intent.putExtra(EXTRA_ACCOUNT, mSelectedConversation.getAccount().getJid().toBareJid().toString());
-			intent.putExtra("choice", attachmentChoice);
-			intent.putExtra("conversation", mSelectedConversation.getUuid());
-			startActivityForResult(intent, requestCode);
-			return true;
-		} else {
-			return false;
-		}
-	}
-
 	@Override
 	protected void refreshUiReal() {
 		updateConversationList();
@@ -1675,9 +893,6 @@ public class ConversationActivity extends XmppActivity
 		this.refreshUi();
 	}
 
-	public void unblockConversation(final Blockable conversation) {
-		xmppConnectionService.sendUnblockRequest(conversation);
-	}
 
 	public boolean enterIsSend() {
 		return getPreferences().getBoolean("enter_is_send", getResources().getBoolean(R.bool.enter_is_send));

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -1,6 +1,11 @@
 package eu.siacs.conversations.ui;
 
+import android.annotation.SuppressLint;
 import android.app.Activity;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.MediaStore;
 import android.support.v7.app.AlertDialog;
 import android.app.Fragment;
 import android.app.PendingIntent;
@@ -21,8 +26,9 @@ import android.util.Pair;
 import android.view.ContextMenu;
 import android.view.ContextMenu.ContextMenuInfo;
 import android.view.Gravity;
-import android.view.KeyEvent;
 import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
@@ -34,6 +40,7 @@ import android.widget.AbsListView;
 import android.widget.AbsListView.OnScrollListener;
 import android.widget.AdapterView;
 import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.CheckBox;
 import android.widget.ImageButton;
 import android.widget.ListView;
 import android.widget.PopupMenu;
@@ -42,17 +49,24 @@ import android.widget.TextView;
 import android.widget.TextView.OnEditorActionListener;
 import android.widget.Toast;
 
+import org.openintents.openpgp.util.OpenPgpApi;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Blockable;
 import eu.siacs.conversations.entities.Contact;
@@ -61,6 +75,7 @@ import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.entities.Presences;
 import eu.siacs.conversations.entities.ReadByMarker;
 import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.entities.TransferablePlaceholder;
@@ -68,10 +83,13 @@ import eu.siacs.conversations.http.HttpDownloadConnection;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.MessageArchiveService;
 import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
-import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
 import eu.siacs.conversations.ui.adapter.MessageAdapter;
+import eu.siacs.conversations.ui.util.ActivityResult;
+import eu.siacs.conversations.ui.util.AttachmentTool;
+import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
+import eu.siacs.conversations.ui.util.PresenceSelector;
 import eu.siacs.conversations.ui.util.SendButtonAction;
+import eu.siacs.conversations.ui.util.SendButtonTool;
 import eu.siacs.conversations.ui.widget.EditMessage;
 import eu.siacs.conversations.utils.MessageUtils;
 import eu.siacs.conversations.utils.NickValidityChecker;
@@ -79,16 +97,35 @@ import eu.siacs.conversations.utils.StylingHelper;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.chatstate.ChatState;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
 
-import static eu.siacs.conversations.ui.ConversationActivity.ATTACHMENT_CHOICE_CHOOSE_IMAGE;
-import static eu.siacs.conversations.ui.ConversationActivity.ATTACHMENT_CHOICE_LOCATION;
-import static eu.siacs.conversations.ui.ConversationActivity.ATTACHMENT_CHOICE_RECORD_VIDEO;
-import static eu.siacs.conversations.ui.ConversationActivity.ATTACHMENT_CHOICE_RECORD_VOICE;
-import static eu.siacs.conversations.ui.ConversationActivity.ATTACHMENT_CHOICE_TAKE_PHOTO;
+import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT;
+import static eu.siacs.conversations.ui.XmppActivity.REQUEST_ANNOUNCE_PGP;
+import static eu.siacs.conversations.ui.XmppActivity.REQUEST_CHOOSE_PGP_ID;
+
 
 public class ConversationFragment extends Fragment implements EditMessage.KeyboardListener {
 
+
+	public static final int REQUEST_SEND_MESSAGE = 0x0201;
+	public static final int REQUEST_DECRYPT_PGP = 0x0202;
+	public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
+	public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208;
+	public static final int REQUEST_TRUST_KEYS_MENU = 0x0209;
+	public static final int REQUEST_START_DOWNLOAD = 0x0210;
+	public static final int REQUEST_ADD_EDITOR_CONTENT = 0x0211;
+	public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
+	public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
+	public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303;
+	public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304;
+	public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305;
+	public static final int ATTACHMENT_CHOICE_INVALID = 0x0306;
+	public static final int ATTACHMENT_CHOICE_RECORD_VIDEO = 0x0307;
+
+	public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action";
+
+
 	final protected List<Message> messageList = new ArrayList<>();
 	protected Conversation conversation;
 	protected ListView messagesView;
@@ -99,6 +136,12 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 	private TextView snackbarMessage;
 	private TextView snackbarAction;
 	private Toast messageLoaderToast;
+
+	private ActivityResult postponedActivityResult = null;
+	public Uri mPendingEditorContent = null;
+
+	private ConversationActivity activity;
+
 	private OnClickListener clickToMuc = new OnClickListener() {
 
 		@Override
@@ -109,7 +152,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 			startActivity(intent);
 		}
 	};
-	private ConversationActivity activity;
 	private OnClickListener leaveMuc = new OnClickListener() {
 
 		@Override
@@ -133,13 +175,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 			if (password == null) {
 				password = "";
 			}
-			activity.quickPasswordEdit(password, new OnValueEdited() {
-
-				@Override
-				public String onValueEdited(String value) {
-					activity.xmppConnectionService.providePasswordForMuc(conversation, value);
-					return null;
-				}
+			activity.quickPasswordEdit(password, value -> {
+				activity.xmppConnectionService.providePasswordForMuc(conversation, value);
+				return null;
 			});
 		}
 	};
@@ -168,53 +206,47 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 								conversation.messagesLoaded.set(true);
 								return;
 							}
-							activity.runOnUiThread(new Runnable() {
-								@Override
-								public void run() {
-									final int oldPosition = messagesView.getFirstVisiblePosition();
-									Message message = null;
-									int childPos;
-									for (childPos = 0; childPos + oldPosition < messageList.size(); ++childPos) {
-										message = messageList.get(oldPosition + childPos);
-										if (message.getType() != Message.TYPE_STATUS) {
-											break;
-										}
+							getActivity().runOnUiThread(() -> {
+								final int oldPosition = messagesView.getFirstVisiblePosition();
+								Message message = null;
+								int childPos;
+								for (childPos = 0; childPos + oldPosition < messageList.size(); ++childPos) {
+									message = messageList.get(oldPosition + childPos);
+									if (message.getType() != Message.TYPE_STATUS) {
+										break;
 									}
-									final String uuid = message != null ? message.getUuid() : null;
-									View v = messagesView.getChildAt(childPos);
-									final int pxOffset = (v == null) ? 0 : v.getTop();
-									ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList);
-									try {
-										updateStatusMessages();
-									} catch (IllegalStateException e) {
-										Log.d(Config.LOGTAG, "caught illegal state exception while updating status messages");
-									}
-									messageListAdapter.notifyDataSetChanged();
-									int pos = Math.max(getIndexOf(uuid, messageList), 0);
-									messagesView.setSelectionFromTop(pos, pxOffset);
-									if (messageLoaderToast != null) {
-										messageLoaderToast.cancel();
-									}
-									conversation.messagesLoaded.set(true);
 								}
+								final String uuid = message != null ? message.getUuid() : null;
+								View v = messagesView.getChildAt(childPos);
+								final int pxOffset = (v == null) ? 0 : v.getTop();
+								ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList);
+								try {
+									updateStatusMessages();
+								} catch (IllegalStateException e) {
+									Log.d(Config.LOGTAG, "caught illegal state exception while updating status messages");
+								}
+								messageListAdapter.notifyDataSetChanged();
+								int pos = Math.max(getIndexOf(uuid, messageList), 0);
+								messagesView.setSelectionFromTop(pos, pxOffset);
+								if (messageLoaderToast != null) {
+									messageLoaderToast.cancel();
+								}
+								conversation.messagesLoaded.set(true);
 							});
 						}
 
 						@Override
 						public void informUser(final int resId) {
 
-							activity.runOnUiThread(new Runnable() {
-								@Override
-								public void run() {
-									if (messageLoaderToast != null) {
-										messageLoaderToast.cancel();
-									}
-									if (ConversationFragment.this.conversation != conversation) {
-										return;
-									}
-									messageLoaderToast = Toast.makeText(view.getContext(), resId, Toast.LENGTH_LONG);
-									messageLoaderToast.show();
+							getActivity().runOnUiThread(() -> {
+								if (messageLoaderToast != null) {
+									messageLoaderToast.cancel();
+								}
+								if (ConversationFragment.this.conversation != conversation) {
+									return;
 								}
+								messageLoaderToast = Toast.makeText(view.getContext(), resId, Toast.LENGTH_LONG);
+								messageLoaderToast.show();
 							});
 
 						}
@@ -234,18 +266,15 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 					inputContentInfo.requestPermission();
 				} catch (Exception e) {
 					Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e);
-					Toast.makeText(
-							activity,
-							activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()),
-							Toast.LENGTH_LONG
+					Toast.makeText(getActivity(),activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()), Toast.LENGTH_LONG
 					).show();
 					return false;
 				}
 			}
-			if (activity.hasStoragePermission(ConversationActivity.REQUEST_ADD_EDITOR_CONTENT)) {
-				activity.attachImageToConversation(inputContentInfo.getContentUri());
+			if (activity.hasStoragePermission(REQUEST_ADD_EDITOR_CONTENT)) {
+				attachImageToConversation(inputContentInfo.getContentUri());
 			} else {
-				activity.mPendingEditorContent = inputContentInfo.getContentUri();
+				mPendingEditorContent = inputContentInfo.getContentUri();
 			}
 			return true;
 		}
@@ -264,25 +293,15 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 	private OnClickListener mUnblockClickListener = new OnClickListener() {
 		@Override
 		public void onClick(final View v) {
-			v.post(new Runnable() {
-				@Override
-				public void run() {
-					v.setVisibility(View.INVISIBLE);
-				}
-			});
+			v.post(() -> v.setVisibility(View.INVISIBLE));
 			if (conversation.isDomainBlocked()) {
 				BlockContactDialog.show(activity, conversation);
 			} else {
-				activity.unblockConversation(conversation);
+				unblockConversation(conversation);
 			}
 		}
 	};
-	private OnClickListener mBlockClickListener = new OnClickListener() {
-		@Override
-		public void onClick(final View view) {
-			showBlockSubmenu(view);
-		}
-	};
+	private OnClickListener mBlockClickListener = this::showBlockSubmenu;
 	private OnClickListener mAddBackClickListener = new OnClickListener() {
 
 		@Override
@@ -294,13 +313,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 			}
 		}
 	};
-	private View.OnLongClickListener mLongPressBlockListener = new View.OnLongClickListener() {
-		@Override
-		public boolean onLongClick(View v) {
-			showBlockSubmenu(v);
-			return true;
-		}
-	};
+	private View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu;
 	private OnClickListener mAllowPresenceSubscription = new OnClickListener() {
 		@Override
 		public void onClick(View v) {
@@ -321,14 +334,14 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 			PendingIntent pendingIntent = conversation.getAccount().getPgpDecryptionService().getPendingIntent();
 			if (pendingIntent != null) {
 				try {
-					activity.startIntentSenderForResult(pendingIntent.getIntentSender(),
-							ConversationActivity.REQUEST_DECRYPT_PGP,
+					getActivity().startIntentSenderForResult(pendingIntent.getIntentSender(),
+							REQUEST_DECRYPT_PGP,
 							null,
 							0,
 							0,
 							0);
 				} catch (SendIntentException e) {
-					Toast.makeText(activity, R.string.unable_to_connect_to_keychain, Toast.LENGTH_SHORT).show();
+					Toast.makeText(getActivity(), R.string.unable_to_connect_to_keychain, Toast.LENGTH_SHORT).show();
 					conversation.getAccount().getPgpDecryptionService().continueDecryption(true);
 				}
 			}
@@ -336,21 +349,17 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 		}
 	};
 	private AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false);
-	private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
-
-		@Override
-		public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
-			if (actionId == EditorInfo.IME_ACTION_SEND) {
-				InputMethodManager imm = (InputMethodManager) v.getContext()
-						.getSystemService(Context.INPUT_METHOD_SERVICE);
-				if (imm.isFullscreenMode()) {
-					imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
-				}
-				sendMessage();
-				return true;
-			} else {
-				return false;
+	private OnEditorActionListener mEditorActionListener = (v, actionId, event) -> {
+		if (actionId == EditorInfo.IME_ACTION_SEND) {
+			InputMethodManager imm = (InputMethodManager) v.getContext()
+					.getSystemService(Context.INPUT_METHOD_SERVICE);
+			if (imm.isFullscreenMode()) {
+				imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
 			}
+			sendMessage();
+			return true;
+		} else {
+			return false;
 		}
 	};
 	private OnClickListener mSendButtonListener = new OnClickListener() {
@@ -366,7 +375,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 					case SEND_LOCATION:
 					case RECORD_VOICE:
 					case CHOOSE_PICTURE:
-						activity.attachFile(action.toChoice());
+						attachFile(action.toChoice());
 						break;
 					case CANCEL:
 						if (conversation != null) {
@@ -395,6 +404,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 	private String incomplete;
 	private int lastCompletionCursor;
 	private boolean firstWord = false;
+	private Message mPendingDownloadableMessage;
 
 	private int getIndexOf(String uuid, List<Message> messages) {
 		if (uuid == null) {
@@ -438,6 +448,104 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 		}
 	}
 
+
+	private void attachLocationToConversation(Conversation conversation, Uri uri) {
+		if (conversation == null) {
+			return;
+		}
+		activity.xmppConnectionService.attachLocationToConversation(conversation, uri, new UiCallback<Message>() {
+
+			@Override
+			public void success(Message message) {
+				activity.xmppConnectionService.sendMessage(message);
+			}
+
+			@Override
+			public void error(int errorCode, Message object) {
+
+			}
+
+			@Override
+			public void userInputRequried(PendingIntent pi, Message object) {
+
+			}
+		});
+	}
+
+	private void attachFileToConversation(Conversation conversation, Uri uri) {
+		if (conversation == null) {
+			return;
+		}
+		final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_file), Toast.LENGTH_LONG);
+		prepareFileToast.show();
+		activity.delegateUriPermissionsToService(uri);
+		activity.xmppConnectionService.attachFileToConversation(conversation, uri, new UiInformableCallback<Message>() {
+			@Override
+			public void inform(final String text) {
+				hidePrepareFileToast(prepareFileToast);
+				getActivity().runOnUiThread(() -> activity.replaceToast(text));
+			}
+
+			@Override
+			public void success(Message message) {
+				getActivity().runOnUiThread(() -> activity.hideToast());
+				hidePrepareFileToast(prepareFileToast);
+				activity.xmppConnectionService.sendMessage(message);
+			}
+
+			@Override
+			public void error(final int errorCode, Message message) {
+				hidePrepareFileToast(prepareFileToast);
+				getActivity().runOnUiThread(() -> activity.replaceToast(getString(errorCode)));
+
+			}
+
+			@Override
+			public void userInputRequried(PendingIntent pi, Message message) {
+				hidePrepareFileToast(prepareFileToast);
+			}
+		});
+	}
+
+	public void attachImageToConversation(Uri uri) {
+		this.attachImageToConversation(conversation, uri);
+	}
+
+	private void attachImageToConversation(Conversation conversation, Uri uri) {
+		if (conversation == null) {
+			return;
+		}
+		final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
+		prepareFileToast.show();
+		activity.delegateUriPermissionsToService(uri);
+		activity.xmppConnectionService.attachImageToConversation(conversation, uri,
+				new UiCallback<Message>() {
+
+					@Override
+					public void userInputRequried(PendingIntent pi, Message object) {
+						hidePrepareFileToast(prepareFileToast);
+					}
+
+					@Override
+					public void success(Message message) {
+						hidePrepareFileToast(prepareFileToast);
+						activity.xmppConnectionService.sendMessage(message);
+					}
+
+					@Override
+					public void error(final int error, Message message) {
+						hidePrepareFileToast(prepareFileToast);
+						activity.runOnUiThread(() -> activity.replaceToast(getString(error)));
+					}
+				});
+	}
+
+	private void hidePrepareFileToast(final Toast prepareFileToast) {
+		if (prepareFileToast != null) {
+			activity.runOnUiThread(prepareFileToast::cancel);
+		}
+	}
+
 	private void sendMessage() {
 		final String body = mEditMessage.getText().toString();
 		final Conversation conversation = this.conversation;
@@ -466,7 +574,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 				sendPgpMessage(message);
 				break;
 			case Message.ENCRYPTION_AXOLOTL:
-				if (!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) {
+				if (!trustKeysIfNeeded(REQUEST_TRUST_KEYS_TEXT)) {
 					sendAxolotlMessage(message);
 				}
 				break;
@@ -475,6 +583,36 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 		}
 	}
 
+	protected boolean trustKeysIfNeeded(int requestCode) {
+		return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID);
+	}
+
+	protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
+		AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
+		final List<Jid> targets = axolotlService.getCryptoTargets(conversation);
+		boolean hasUnaccepted = !conversation.getAcceptedCryptoTargets().containsAll(targets);
+		boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty();
+		boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty();
+		boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(conversation).isEmpty();
+		boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
+		if (hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) {
+			axolotlService.createSessionsIfNeeded(conversation);
+			Intent intent = new Intent(getActivity(), TrustKeysActivity.class);
+			String[] contacts = new String[targets.size()];
+			for (int i = 0; i < contacts.length; ++i) {
+				contacts[i] = targets.get(i).toString();
+			}
+			intent.putExtra("contacts", contacts);
+			intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().toBareJid().toString());
+			intent.putExtra("choice", attachmentChoice);
+			intent.putExtra("conversation", conversation.getUuid());
+			startActivityForResult(intent, requestCode);
+			return true;
+		} else {
+			return false;
+		}
+	}
+
 	public void updateChatMsgHint() {
 		final boolean multi = conversation.getMode() == Conversation.MODE_MULTI;
 		if (conversation.getCorrectingMessage() != null) {
@@ -486,12 +624,12 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 		} else if (multi && !conversation.getMucOptions().participating()) {
 			this.mEditMessage.setHint(R.string.you_are_not_participating);
 		} else {
-			this.mEditMessage.setHint(UIHelper.getMessageHint(activity, conversation));
+			this.mEditMessage.setHint(UIHelper.getMessageHint(getActivity(), conversation));
 			getActivity().invalidateOptionsMenu();
 		}
 	}
 
-	public void setupIme() {
+	public void setupIme() {;
 		if (activity != null) {
 			if (activity.usingEnterKey() && activity.enterIsSend()) {
 				mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
@@ -506,19 +644,153 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 		}
 	}
 
+	private void handleActivityResult(ActivityResult activityResult) {
+		if (activityResult.resultCode == Activity.RESULT_OK) {
+			handlePositiveActivityResult(activityResult.requestCode, activityResult.data);
+		} else {
+			handleNegativeActivityResult(activityResult.requestCode);
+		}
+	}
+
+	private void handlePositiveActivityResult(int requestCode, final Intent data) {
+		switch (requestCode) {
+			case REQUEST_DECRYPT_PGP:
+				conversation.getAccount().getPgpDecryptionService().continueDecryption(data);
+				break;
+			case REQUEST_TRUST_KEYS_TEXT:
+				final String body = mEditMessage.getText().toString();
+				Message message = new Message(conversation, body, conversation.getNextEncryption());
+				sendAxolotlMessage(message);
+				break;
+			case REQUEST_TRUST_KEYS_MENU:
+				int choice = data.getIntExtra("choice", ATTACHMENT_CHOICE_INVALID);
+				selectPresenceToAttachFile(choice);
+				break;
+			case REQUEST_CHOOSE_PGP_ID:
+				long id = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID,0);
+				if (id != 0) {
+					conversation.getAccount().setPgpSignId(id);
+					activity.announcePgp(conversation.getAccount(),null,null,activity.onOpenPGPKeyPublished);
+				} else {
+					activity.choosePgpSignId(conversation.getAccount());
+				}
+				break;
+			case REQUEST_ANNOUNCE_PGP:
+				activity.announcePgp(conversation.getAccount(), conversation, data, activity.onOpenPGPKeyPublished);
+				break;
+			case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
+				List<Uri> imageUris = AttachmentTool.extractUriFromIntent(data);
+				for (Iterator<Uri> i = imageUris.iterator(); i.hasNext(); i.remove()) {
+					Log.d(Config.LOGTAG, "ConversationsActivity.onActivityResult() - attaching image to conversations. CHOOSE_IMAGE");
+					attachImageToConversation(conversation, i.next());
+				}
+				break;
+			case ATTACHMENT_CHOICE_CHOOSE_FILE:
+			case ATTACHMENT_CHOICE_RECORD_VIDEO:
+			case ATTACHMENT_CHOICE_RECORD_VOICE:
+				final List<Uri> fileUris = AttachmentTool.extractUriFromIntent(data);
+				final PresenceSelector.OnPresenceSelected callback = () -> {
+					for (Iterator<Uri> i = fileUris.iterator(); i.hasNext(); i.remove()) {
+						Log.d(Config.LOGTAG, "ConversationsActivity.onActivityResult() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
+						attachFileToConversation(conversation, i.next());
+					}
+				};
+				if (conversation == null || conversation.getMode() == Conversation.MODE_MULTI || FileBackend.allFilesUnderSize(getActivity(), fileUris, activity.getMaxHttpUploadSize(conversation))) {
+					callback.onPresenceSelected();
+				} else {
+					activity.selectPresence(conversation, callback);
+				}
+				break;
+			case ATTACHMENT_CHOICE_LOCATION:
+				double latitude = data.getDoubleExtra("latitude", 0);
+				double longitude = data.getDoubleExtra("longitude", 0);
+				Uri geo = Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude));
+				attachLocationToConversation(conversation, geo);
+				break;
+		}
+	}
+
+	private void handleNegativeActivityResult(int requestCode) {
+		switch (requestCode) {
+			case REQUEST_DECRYPT_PGP:
+				// discard the message to prevent decryption being blocked
+				conversation.getAccount().getPgpDecryptionService().giveUpCurrentDecryption();
+				break;
+		}
+	}
+
+	@Override
+	public void onActivityResult(int requestCode, int resultCode, final Intent data) {
+		super.onActivityResult(requestCode, resultCode, data);
+		ActivityResult activityResult = ActivityResult.of(requestCode,resultCode,data);
+		if (activity != null && activity.xmppConnectionService != null) {
+			handleActivityResult(activityResult);
+		} else {
+			this.postponedActivityResult = activityResult;
+		}
+	}
+
+	public void unblockConversation(final Blockable conversation) {
+		activity.xmppConnectionService.sendUnblockRequest(conversation);
+	}
+
+	@Override
+	public void onAttach(Context context) {
+		if (context instanceof ConversationActivity) {
+			this.activity = (ConversationActivity) context;
+		} else {
+			throw new IllegalStateException("Trying to attach fragment to activity that is not the ConversationActivity");
+		}
+		super.onAttach(context);
+	}
+
+	@Override
+	public void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		setHasOptionsMenu(true);
+	}
+
+
+	@Override
+	public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+		menuInflater.inflate(R.menu.fragment_conversation, menu);
+		final MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details);
+		final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details);
+		final MenuItem menuInviteContact = menu.findItem(R.id.action_invite);
+		final MenuItem menuMute = menu.findItem(R.id.action_mute);
+		final MenuItem menuUnmute = menu.findItem(R.id.action_unmute);
+
+
+		if (conversation != null) {
+			if (conversation.getMode() == Conversation.MODE_MULTI) {
+				menuContactDetails.setVisible(false);
+				menuInviteContact.setVisible(conversation.getMucOptions().canInvite());
+			} else {
+				menuContactDetails.setVisible(!this.conversation.withSelf());
+				menuMucDetails.setVisible(false);
+				final XmppConnectionService service = activity.xmppConnectionService;
+				menuInviteContact.setVisible(service != null && service.findConferenceServer(conversation.getAccount()) != null);
+			}
+			if (conversation.isMuted()) {
+				menuMute.setVisible(false);
+			} else {
+				menuUnmute.setVisible(false);
+			}
+			ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu);
+			ConversationMenuConfigurator.configureEncryptionMenu(conversation, menu);
+		}
+		super.onCreateOptionsMenu(menu, menuInflater);
+	}
+
 	@Override
 	public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 		final View view = inflater.inflate(R.layout.fragment_conversation, container, false);
 		view.setOnClickListener(null);
 
 		mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
-		mEditMessage.setOnClickListener(new OnClickListener() {
-
-			@Override
-			public void onClick(View v) {
-				if (activity != null) {
-					activity.hideConversationsOverview();
-				}
+		mEditMessage.setOnClickListener(v -> {
+			if (activity != null) {
+				activity.hideConversationsOverview();
 			}
 		});
 
@@ -545,7 +817,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 					Jid user = message.getCounterpart();
 					if (user != null && !user.isBareJid()) {
 						if (!message.getConversation().getMucOptions().isUserInRoom(user)) {
-							Toast.makeText(activity, activity.getString(R.string.user_has_left_conference, user.getResourcepart()), Toast.LENGTH_SHORT).show();
+							Toast.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, user.getResourcepart()), Toast.LENGTH_SHORT).show();
 						}
 						highlightInConference(user.getResourcepart());
 					}
@@ -568,7 +840,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 			Intent intent;
 			if (activity.manuallyChangePresence() && !received) {
 				intent = new Intent(activity, SetPresenceActivity.class);
-				intent.putExtra(SetPresenceActivity.EXTRA_ACCOUNT, account.getJid().toBareJid().toString());
+				intent.putExtra(EXTRA_ACCOUNT, account.getJid().toBareJid().toString());
 			} else {
 				intent = new Intent(activity, EditAccountActivity.class);
 				intent.putExtra("jid", account.getJid().toBareJid().toString());
@@ -588,7 +860,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 				if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
 					final MucOptions mucOptions = conversation.getMucOptions();
 					if (!mucOptions.allowPm()) {
-						Toast.makeText(activity, R.string.private_messages_are_disabled, Toast.LENGTH_SHORT).show();
+						Toast.makeText(getActivity(), R.string.private_messages_are_disabled, Toast.LENGTH_SHORT).show();
 						return;
 					}
 					Jid user = message.getCounterpart();
@@ -596,7 +868,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 						if (mucOptions.isUserInRoom(user)) {
 							privateMessageWith(user);
 						} else {
-							Toast.makeText(activity, activity.getString(R.string.user_has_left_conference, user.getResourcepart()), Toast.LENGTH_SHORT).show();
+							Toast.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, user.getResourcepart()), Toast.LENGTH_SHORT).show();
 						}
 					}
 				}
@@ -765,8 +1037,356 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 		}
 	}
 
+	@Override
+	public boolean onOptionsItemSelected(final MenuItem item) {
+		if (conversation == null) {
+			return super.onOptionsItemSelected(item);
+		}
+		switch (item.getItemId()) {
+			case R.id.encryption_choice_axolotl:
+			case R.id.encryption_choice_pgp:
+			case R.id.encryption_choice_none:
+				handleEncryptionSelection(item);
+				break;
+			case R.id.attach_choose_picture:
+			case R.id.attach_take_picture:
+			case R.id.attach_record_video:
+			case R.id.attach_choose_file:
+			case R.id.attach_record_voice:
+			case R.id.attach_location:
+				handleAttachmentSelection(item);
+				break;
+			case R.id.action_archive:
+				activity.endConversation(conversation);
+				break;
+			case R.id.action_contact_details:
+				activity.switchToContactDetails(conversation.getContact());
+				break;
+			case R.id.action_muc_details:
+				Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
+				intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
+				intent.putExtra("uuid", conversation.getUuid());
+				startActivity(intent);
+				break;
+			case R.id.action_invite:
+				activity.inviteToConversation(conversation);
+				break;
+			case R.id.action_clear_history:
+				clearHistoryDialog(conversation);
+				break;
+			case R.id.action_mute:
+				muteConversationDialog(conversation);
+				break;
+			case R.id.action_unmute:
+				unmuteConversation(conversation);
+				break;
+			case R.id.action_block:
+			case R.id.action_unblock:
+				final Activity activity = getActivity();
+				if (activity instanceof XmppActivity) {
+					BlockContactDialog.show((XmppActivity) activity, conversation);
+				}
+				break;
+			default:
+				break;
+		}
+		return super.onOptionsItemSelected(item);
+	}
+
+	private void handleAttachmentSelection(MenuItem item) {
+		switch (item.getItemId()) {
+			case R.id.attach_choose_picture:
+				attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
+				break;
+			case R.id.attach_take_picture:
+				attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
+				break;
+			case R.id.attach_record_video:
+				attachFile(ATTACHMENT_CHOICE_RECORD_VIDEO);
+				break;
+			case R.id.attach_choose_file:
+				attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE);
+				break;
+			case R.id.attach_record_voice:
+				attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
+				break;
+			case R.id.attach_location:
+				attachFile(ATTACHMENT_CHOICE_LOCATION);
+				break;
+		}
+	}
+
+	private void handleEncryptionSelection(MenuItem item) {
+		if (conversation == null) {
+			return;
+		}
+		final ConversationFragment fragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation");
+		switch (item.getItemId()) {
+			case R.id.encryption_choice_none:
+				conversation.setNextEncryption(Message.ENCRYPTION_NONE);
+				item.setChecked(true);
+				break;
+			case R.id.encryption_choice_pgp:
+				if (activity.hasPgp()) {
+					if (conversation.getAccount().getPgpSignature() != null) {
+						conversation.setNextEncryption(Message.ENCRYPTION_PGP);
+						item.setChecked(true);
+					} else {
+						activity.announcePgp(conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished);
+					}
+				} else {
+					activity.showInstallPgpDialog();
+				}
+				break;
+			case R.id.encryption_choice_axolotl:
+				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount())
+						+ "Enabled axolotl for Contact " + conversation.getContact().getJid());
+				conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL);
+				item.setChecked(true);
+				break;
+			default:
+				conversation.setNextEncryption(Message.ENCRYPTION_NONE);
+				break;
+		}
+		activity.xmppConnectionService.updateConversation(conversation);
+		fragment.updateChatMsgHint();
+		getActivity().invalidateOptionsMenu();
+		activity.refreshUi();
+	}
+
+	public void attachFile(final int attachmentChoice) {
+		if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) {
+			if (!Config.ONLY_INTERNAL_STORAGE && !activity.hasStoragePermission(attachmentChoice)) {
+				return;
+			}
+		}
+		try {
+			activity.getPreferences().edit()
+					.putString(RECENTLY_USED_QUICK_ACTION, SendButtonAction.of(attachmentChoice).toString())
+					.apply();
+		} catch (IllegalArgumentException e) {
+			//just do not save
+		}
+		final int encryption = conversation.getNextEncryption();
+		final int mode = conversation.getMode();
+		if (encryption == Message.ENCRYPTION_PGP) {
+			if (activity.hasPgp()) {
+				if (mode == Conversation.MODE_SINGLE && conversation.getContact().getPgpKeyId() != 0) {
+					activity.xmppConnectionService.getPgpEngine().hasKey(
+							conversation.getContact(),
+							new UiCallback<Contact>() {
+
+								@Override
+								public void userInputRequried(PendingIntent pi, Contact contact) {
+									activity.runIntent(pi, attachmentChoice);
+								}
+
+								@Override
+								public void success(Contact contact) {
+									selectPresenceToAttachFile(attachmentChoice);
+								}
+
+								@Override
+								public void error(int error, Contact contact) {
+									activity.replaceToast(getString(error));
+								}
+							});
+				} else if (mode == Conversation.MODE_MULTI && conversation.getMucOptions().pgpKeysInUse()) {
+					if (!conversation.getMucOptions().everybodyHasKeys()) {
+						Toast warning = Toast.makeText(getActivity(), R.string.missing_public_keys, Toast.LENGTH_LONG);
+						warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
+						warning.show();
+					}
+					selectPresenceToAttachFile(attachmentChoice);
+				} else {
+					final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
+							.findFragmentByTag("conversation");
+					if (fragment != null) {
+						fragment.showNoPGPKeyDialog(false, (dialog, which) -> {
+									conversation.setNextEncryption(Message.ENCRYPTION_NONE);
+									activity.xmppConnectionService.updateConversation(conversation);
+									selectPresenceToAttachFile(attachmentChoice);
+								});
+					}
+				}
+			} else {
+				activity.showInstallPgpDialog();
+			}
+		} else {
+			if (encryption != Message.ENCRYPTION_AXOLOTL || !trustKeysIfNeeded(REQUEST_TRUST_KEYS_MENU, attachmentChoice)) {
+				selectPresenceToAttachFile(attachmentChoice);
+			}
+		}
+	}
+
+	@Override
+	public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
+		if (grantResults.length > 0)
+			if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+				if (requestCode == REQUEST_START_DOWNLOAD) {
+					if (this.mPendingDownloadableMessage != null) {
+						startDownloadable(this.mPendingDownloadableMessage);
+					}
+				} else if (requestCode == REQUEST_ADD_EDITOR_CONTENT) {
+					if (this.mPendingEditorContent != null) {
+						attachImageToConversation(this.mPendingEditorContent);
+					}
+				} else {
+					attachFile(requestCode);
+				}
+			} else {
+				Toast.makeText(getActivity(), R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
+			}
+	}
+
+	public void startDownloadable(Message message) {
+		if (!Config.ONLY_INTERNAL_STORAGE && !activity.hasStoragePermission(REQUEST_START_DOWNLOAD)) {
+			this.mPendingDownloadableMessage = message;
+			return;
+		}
+		Transferable transferable = message.getTransferable();
+		if (transferable != null) {
+			if (!transferable.start()) {
+				Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
+			}
+		} else if (message.treatAsDownloadable()) {
+			activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true);
+		}
+	}
+
+	@SuppressLint("InflateParams")
+	protected void clearHistoryDialog(final Conversation conversation) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+		builder.setTitle(getString(R.string.clear_conversation_history));
+		final View dialogView = getActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
+		final CheckBox endConversationCheckBox = dialogView.findViewById(R.id.end_conversation_checkbox);
+		builder.setView(dialogView);
+		builder.setNegativeButton(getString(R.string.cancel), null);
+		builder.setPositiveButton(getString(R.string.delete_messages), (dialog, which) -> {
+			this.activity.xmppConnectionService.clearConversationHistory(conversation);
+			if (endConversationCheckBox.isChecked()) {
+				this.activity.endConversation(conversation);
+			} else {
+				activity.updateConversationList();
+				updateMessages();
+			}
+		});
+		builder.create().show();
+	}
+
+	protected void muteConversationDialog(final Conversation conversation) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+		builder.setTitle(R.string.disable_notifications);
+		final int[] durations = getResources().getIntArray(R.array.mute_options_durations);
+		builder.setItems(R.array.mute_options_descriptions, (dialog, which) -> {
+			final long till;
+			if (durations[which] == -1) {
+				till = Long.MAX_VALUE;
+			} else {
+				till = System.currentTimeMillis() + (durations[which] * 1000);
+			}
+			conversation.setMutedTill(till);
+			activity.xmppConnectionService.updateConversation(conversation);
+			activity.updateConversationList();
+			updateMessages();
+			getActivity().invalidateOptionsMenu();
+		});
+		builder.create().show();
+	}
+
+	public void unmuteConversation(final Conversation conversation) {
+		conversation.setMutedTill(0);
+		this.activity.xmppConnectionService.updateConversation(conversation);
+		this.activity.updateConversationList();
+		updateMessages();
+		getActivity().invalidateOptionsMenu();
+	}
+
+	protected void selectPresenceToAttachFile(final int attachmentChoice) {
+		final Account account = conversation.getAccount();
+		final PresenceSelector.OnPresenceSelected callback = () -> {
+			Intent intent = new Intent();
+			boolean chooser = false;
+			String fallbackPackageId = null;
+			switch (attachmentChoice) {
+				case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
+					intent.setAction(Intent.ACTION_GET_CONTENT);
+					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+						intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+					}
+					intent.setType("image/*");
+					chooser = true;
+					break;
+				case ATTACHMENT_CHOICE_RECORD_VIDEO:
+					intent.setAction(MediaStore.ACTION_VIDEO_CAPTURE);
+					break;
+				case ATTACHMENT_CHOICE_TAKE_PHOTO:
+					Uri uri = activity.xmppConnectionService.getFileBackend().getTakePhotoUri();
+					//mPendingImageUris.clear();
+					//mPendingImageUris.add(uri);
+					intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
+					intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+					intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+					intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
+					break;
+				case ATTACHMENT_CHOICE_CHOOSE_FILE:
+					chooser = true;
+					intent.setType("*/*");
+					intent.addCategory(Intent.CATEGORY_OPENABLE);
+					intent.setAction(Intent.ACTION_GET_CONTENT);
+					break;
+				case ATTACHMENT_CHOICE_RECORD_VOICE:
+					intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
+					fallbackPackageId = "eu.siacs.conversations.voicerecorder";
+					break;
+				case ATTACHMENT_CHOICE_LOCATION:
+					intent.setAction("eu.siacs.conversations.location.request");
+					fallbackPackageId = "eu.siacs.conversations.sharelocation";
+					break;
+			}
+			if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
+				if (chooser) {
+					startActivityForResult(
+							Intent.createChooser(intent, getString(R.string.perform_action_with)),
+							attachmentChoice);
+				} else {
+					startActivityForResult(intent, attachmentChoice);
+				}
+			} else if (fallbackPackageId != null) {
+				startActivity(getInstallApkIntent(fallbackPackageId));
+			}
+		};
+		if (account.httpUploadAvailable() || attachmentChoice == ATTACHMENT_CHOICE_LOCATION) {
+			conversation.setNextCounterpart(null);
+			callback.onPresenceSelected();
+		} else {
+			activity.selectPresence(conversation, callback);
+		}
+	}
+
+	private Intent getInstallApkIntent(final String packageId) {
+		Intent intent = new Intent(Intent.ACTION_VIEW);
+		intent.setData(Uri.parse("market://details?id=" + packageId));
+		if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
+			return intent;
+		} else {
+			intent.setData(Uri.parse("http://play.google.com/store/apps/details?id=" + packageId));
+			return intent;
+		}
+	}
+
+	@Override
+	public void onResume() {
+		new Handler().post(() -> {
+			final PackageManager packageManager = getActivity().getPackageManager();
+			ConversationMenuConfigurator.updateAttachmentAvailability(packageManager);
+			getActivity().invalidateOptionsMenu();
+		});
+		super.onResume();
+	}
+
 	private void showErrorMessage(final Message message) {
-		AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
 		builder.setTitle(R.string.error_message);
 		builder.setMessage(message.getErrorMessage());
 		builder.setPositiveButton(R.string.confirm, null);

src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java 🔗

@@ -29,6 +29,7 @@ import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.adapter.ConversationAdapter;
 import eu.siacs.conversations.ui.service.EmojiService;
+import eu.siacs.conversations.ui.util.PresenceSelector;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
@@ -312,7 +313,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
 			return;
 		}
 		if (share.uris.size() != 0) {
-			OnPresenceSelected callback = () -> {
+			PresenceSelector.OnPresenceSelected callback = () -> {
 				attachmentCounter.set(share.uris.size());
 				if (share.image) {
 					share.multiple = share.uris.size() > 1;
@@ -339,7 +340,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
 			}
 		} else {
 			if (mReturnToPrevious && this.share.text != null && !this.share.text.isEmpty() ) {
-				final OnPresenceSelected callback = new OnPresenceSelected() {
+				final PresenceSelector.OnPresenceSelected callback = new PresenceSelector.OnPresenceSelected() {
 
 					private void finishAndSend(Message message) {
 						xmppConnectionService.sendMessage(message);

src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java 🔗

@@ -388,7 +388,7 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
 
 	private void finishOk() {
 		Intent data = new Intent();
-		data.putExtra("choice", getIntent().getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID));
+		data.putExtra("choice", getIntent().getIntExtra("choice", ConversationFragment.ATTACHMENT_CHOICE_INVALID));
 		setResult(RESULT_OK, data);
 		finish();
 	}

src/main/java/eu/siacs/conversations/ui/XmppActivity.java 🔗

@@ -70,6 +70,7 @@ import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.services.BarcodeProvider;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
+import eu.siacs.conversations.ui.util.PresenceSelector;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.ExceptionHelper;
 import eu.siacs.conversations.utils.UIHelper;
@@ -103,7 +104,7 @@ public abstract class XmppActivity extends AppCompatActivity {
 	protected int mTheme;
 	protected boolean mUsingEnterKey = false;
 	protected Toast mToast;
-	protected Runnable onOpenPGPKeyPublished = () -> Toast.makeText(XmppActivity.this, R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show();
+	public Runnable onOpenPGPKeyPublished = () -> Toast.makeText(XmppActivity.this, R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show();
 	protected ConferenceInvite mPendingConferenceInvite = null;
 	protected ServiceConnection mConnection = new ServiceConnection() {
 
@@ -375,6 +376,38 @@ public abstract class XmppActivity extends AppCompatActivity {
 		return super.onOptionsItemSelected(item);
 	}
 
+	public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
+		final Contact contact = conversation.getContact();
+		if (!contact.showInRoster()) {
+			showAddToRosterDialog(conversation.getContact());
+		} else {
+			final Presences presences = contact.getPresences();
+			if (presences.size() == 0) {
+				if (!contact.getOption(Contact.Options.TO)
+						&& !contact.getOption(Contact.Options.ASKING)
+						&& contact.getAccount().getStatus() == Account.State.ONLINE) {
+					showAskForPresenceDialog(contact);
+				} else if (!contact.getOption(Contact.Options.TO)
+						|| !contact.getOption(Contact.Options.FROM)) {
+					PresenceSelector.warnMutualPresenceSubscription(this, conversation, listener);
+				} else {
+					conversation.setNextCounterpart(null);
+					listener.onPresenceSelected();
+				}
+			} else if (presences.size() == 1) {
+				String presence = presences.toResourceArray()[0];
+				try {
+					conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), presence));
+				} catch (InvalidJidException e) {
+					conversation.setNextCounterpart(null);
+				}
+				listener.onPresenceSelected();
+			} else {
+				PresenceSelector.showPresenceSelectionDialog(this, conversation, listener);
+			}
+		}
+	}
+
 	@Override
 	protected void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
@@ -665,10 +698,6 @@ public abstract class XmppActivity extends AppCompatActivity {
 
 	}
 
-	protected void showAddToRosterDialog(final Conversation conversation) {
-		showAddToRosterDialog(conversation.getContact());
-	}
-
 	protected void showAddToRosterDialog(final Contact contact) {
 		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 		builder.setTitle(contact.getJid().toString());
@@ -701,21 +730,6 @@ public abstract class XmppActivity extends AppCompatActivity {
 		builder.create().show();
 	}
 
-	private void warnMutalPresenceSubscription(final Conversation conversation,
-	                                           final OnPresenceSelected listener) {
-		AlertDialog.Builder builder = new AlertDialog.Builder(this);
-		builder.setTitle(conversation.getContact().getJid().toString());
-		builder.setMessage(R.string.without_mutual_presence_updates);
-		builder.setNegativeButton(R.string.cancel, null);
-		builder.setPositiveButton(R.string.ignore, (dialog, which) -> {
-			conversation.setNextCounterpart(null);
-			if (listener != null) {
-				listener.onPresenceSelected();
-			}
-		});
-		builder.create().show();
-	}
-
 	protected void quickEdit(String previousValue, int hint, OnValueEdited callback) {
 		quickEdit(previousValue, callback, hint, false);
 	}
@@ -776,89 +790,6 @@ public abstract class XmppActivity extends AppCompatActivity {
 		}
 	}
 
-	public void selectPresence(final Conversation conversation,
-	                           final OnPresenceSelected listener) {
-		final Contact contact = conversation.getContact();
-		if (!contact.showInRoster()) {
-			showAddToRosterDialog(conversation);
-		} else {
-			final Presences presences = contact.getPresences();
-			if (presences.size() == 0) {
-				if (!contact.getOption(Contact.Options.TO)
-						&& !contact.getOption(Contact.Options.ASKING)
-						&& contact.getAccount().getStatus() == Account.State.ONLINE) {
-					showAskForPresenceDialog(contact);
-				} else if (!contact.getOption(Contact.Options.TO)
-						|| !contact.getOption(Contact.Options.FROM)) {
-					warnMutalPresenceSubscription(conversation, listener);
-				} else {
-					conversation.setNextCounterpart(null);
-					listener.onPresenceSelected();
-				}
-			} else if (presences.size() == 1) {
-				String presence = presences.toResourceArray()[0];
-				try {
-					conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), presence));
-				} catch (InvalidJidException e) {
-					conversation.setNextCounterpart(null);
-				}
-				listener.onPresenceSelected();
-			} else {
-				showPresenceSelectionDialog(presences, conversation, listener);
-			}
-		}
-	}
-
-	private void showPresenceSelectionDialog(Presences presences, final Conversation conversation, final OnPresenceSelected listener) {
-		final Contact contact = conversation.getContact();
-		AlertDialog.Builder builder = new AlertDialog.Builder(this);
-		builder.setTitle(getString(R.string.choose_presence));
-		final String[] resourceArray = presences.toResourceArray();
-		Pair<Map<String, String>, Map<String, String>> typeAndName = presences.toTypeAndNameMap();
-		final Map<String, String> resourceTypeMap = typeAndName.first;
-		final Map<String, String> resourceNameMap = typeAndName.second;
-		final String[] readableIdentities = new String[resourceArray.length];
-		final AtomicInteger selectedResource = new AtomicInteger(0);
-		for (int i = 0; i < resourceArray.length; ++i) {
-			String resource = resourceArray[i];
-			if (resource.equals(contact.getLastResource())) {
-				selectedResource.set(i);
-			}
-			String type = resourceTypeMap.get(resource);
-			String name = resourceNameMap.get(resource);
-			if (type != null) {
-				if (Collections.frequency(resourceTypeMap.values(), type) == 1) {
-					readableIdentities[i] = UIHelper.tranlasteType(this, type);
-				} else if (name != null) {
-					if (Collections.frequency(resourceNameMap.values(), name) == 1
-							|| CryptoHelper.UUID_PATTERN.matcher(resource).matches()) {
-						readableIdentities[i] = UIHelper.tranlasteType(this, type) + "  (" + name + ")";
-					} else {
-						readableIdentities[i] = UIHelper.tranlasteType(this, type) + " (" + name + " / " + resource + ")";
-					}
-				} else {
-					readableIdentities[i] = UIHelper.tranlasteType(this, type) + " (" + resource + ")";
-				}
-			} else {
-				readableIdentities[i] = resource;
-			}
-		}
-		builder.setSingleChoiceItems(readableIdentities,
-				selectedResource.get(),
-				(dialog, which) -> selectedResource.set(which));
-		builder.setNegativeButton(R.string.cancel, null);
-		builder.setPositiveButton(R.string.ok, (dialog, which) -> {
-			try {
-				Jid next = Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), resourceArray[selectedResource.get()]);
-				conversation.setNextCounterpart(next);
-			} catch (InvalidJidException e) {
-				conversation.setNextCounterpart(null);
-			}
-			listener.onPresenceSelected();
-		});
-		builder.create().show();
-	}
-
 	protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
 		super.onActivityResult(requestCode, resultCode, data);
 		if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
@@ -1048,10 +979,6 @@ public abstract class XmppActivity extends AppCompatActivity {
 		String onValueEdited(String value);
 	}
 
-	public interface OnPresenceSelected {
-		void onPresenceSelected();
-	}
-
 	public static class ConferenceInvite {
 		private String uuid;
 		private List<Jid> jids = new ArrayList<>();

src/main/java/eu/siacs/conversations/ui/util/ActivityResult.java 🔗

@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2018, Daniel Gultsch All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package eu.siacs.conversations.ui.util;
+
+import android.content.Intent;
+
+public class ActivityResult {
+
+	public final int requestCode;
+	public final int resultCode;
+	public final Intent data;
+
+	private ActivityResult(int requestCode, int resultCode, final Intent data) {
+		this.requestCode = requestCode;
+		this.resultCode = resultCode;
+		this.data = data;
+	}
+
+	public static ActivityResult of(int requestCode, int resultCode, Intent data) {
+		return new ActivityResult(requestCode,resultCode,data);
+	}
+
+}

src/main/java/eu/siacs/conversations/ui/util/AttachmentTool.java 🔗

@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2018, Daniel Gultsch All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package eu.siacs.conversations.ui.util;
+
+import android.annotation.SuppressLint;
+import android.content.ClipData;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AttachmentTool {
+	@SuppressLint("NewApi")
+	public static List<Uri> extractUriFromIntent(final Intent intent) {
+		List<Uri> uris = new ArrayList<>();
+		if (intent == null) {
+			return uris;
+		}
+		Uri uri = intent.getData();
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && uri == null) {
+			final ClipData clipData = intent.getClipData();
+			if (clipData != null) {
+				for (int i = 0; i < clipData.getItemCount(); ++i) {
+					uris.add(clipData.getItemAt(i).getUri());
+				}
+			}
+		} else {
+			uris.add(uri);
+		}
+		return uris;
+	}
+}

src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java 🔗

@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2018, Daniel Gultsch All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package eu.siacs.conversations.ui.util;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.provider.MediaStore;
+import android.support.annotation.NonNull;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+
+public class ConversationMenuConfigurator {
+
+	private static boolean showSoundRecorderAttachment = false;
+	private static boolean showLocationAttachment = false;
+
+
+	public static void configureAttachmentMenu(@NonNull Conversation conversation, Menu menu) {
+		final MenuItem menuAttachSoundRecorder = menu.findItem(R.id.attach_record_voice);
+		final MenuItem menuAttachLocation = menu.findItem(R.id.attach_location);
+		final MenuItem menuAttach = menu.findItem(R.id.action_attach_file);
+
+		final boolean visible;
+		if (conversation.getMode() == Conversation.MODE_MULTI) {
+			visible = conversation.getAccount().httpUploadAvailable() && conversation.getMucOptions().participating();
+		} else {
+			visible = true;
+		}
+
+		menuAttach.setVisible(visible);
+
+		if (!visible) {
+			return;
+		}
+
+		menuAttachLocation.setVisible(showLocationAttachment);
+		menuAttachSoundRecorder.setVisible(showSoundRecorderAttachment);
+	}
+
+	public static void configureEncryptionMenu(@NonNull Conversation conversation, Menu menu) {
+		final MenuItem menuSecure = menu.findItem(R.id.action_security);
+		final MenuItem none = menu.findItem(R.id.encryption_choice_none);
+		final MenuItem pgp = menu.findItem(R.id.encryption_choice_pgp);
+		final MenuItem axolotl = menu.findItem(R.id.encryption_choice_axolotl);
+
+		boolean visible;
+		if (conversation.getMode() == Conversation.MODE_MULTI) {
+			visible = (Config.supportOpenPgp() || Config.supportOmemo()) && Config.multipleEncryptionChoices();
+		} else {
+			visible = Config.multipleEncryptionChoices();
+		}
+
+		menuSecure.setVisible(visible);
+
+		if (!visible) {
+			return;
+		}
+
+		if (conversation.getNextEncryption() != Message.ENCRYPTION_NONE) {
+			menuSecure.setIcon(R.drawable.ic_lock_white_24dp);
+		}
+
+		pgp.setVisible(Config.supportOpenPgp());
+		none.setVisible(Config.supportUnencrypted() || conversation.getMode() == Conversation.MODE_MULTI);
+		axolotl.setVisible(Config.supportOmemo());
+		final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
+		if (axolotlService == null || !axolotlService.isConversationAxolotlCapable(conversation)) {
+			axolotl.setEnabled(false);
+		}
+		switch (conversation.getNextEncryption()) {
+			case Message.ENCRYPTION_NONE:
+				none.setChecked(true);
+				break;
+			case Message.ENCRYPTION_PGP:
+				pgp.setChecked(true);
+				break;
+			case Message.ENCRYPTION_AXOLOTL:
+				axolotl.setChecked(true);
+				break;
+			default:
+				none.setChecked(true);
+				break;
+		}
+	}
+
+	public static void updateAttachmentAvailability(PackageManager packageManager) {
+		showSoundRecorderAttachment = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(packageManager) != null;
+		showLocationAttachment = new Intent("eu.siacs.conversations.location.request").resolveActivity(packageManager) != null;
+	}
+}

src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java 🔗

@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2018, Daniel Gultsch All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package eu.siacs.conversations.ui.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.support.v7.app.AlertDialog;
+import android.util.Pair;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class PresenceSelector {
+
+	public static void showPresenceSelectionDialog(Activity activity, final Conversation conversation, final OnPresenceSelected listener) {
+		final Contact contact = conversation.getContact();
+		final Presences presences = contact.getPresences();
+		AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+		builder.setTitle(activity.getString(R.string.choose_presence));
+		final String[] resourceArray = presences.toResourceArray();
+		Pair<Map<String, String>, Map<String, String>> typeAndName = presences.toTypeAndNameMap();
+		final Map<String, String> resourceTypeMap = typeAndName.first;
+		final Map<String, String> resourceNameMap = typeAndName.second;
+		final String[] readableIdentities = new String[resourceArray.length];
+		final AtomicInteger selectedResource = new AtomicInteger(0);
+		for (int i = 0; i < resourceArray.length; ++i) {
+			String resource = resourceArray[i];
+			if (resource.equals(contact.getLastResource())) {
+				selectedResource.set(i);
+			}
+			String type = resourceTypeMap.get(resource);
+			String name = resourceNameMap.get(resource);
+			if (type != null) {
+				if (Collections.frequency(resourceTypeMap.values(), type) == 1) {
+					readableIdentities[i] = translateType(activity, type);
+				} else if (name != null) {
+					if (Collections.frequency(resourceNameMap.values(), name) == 1
+							|| CryptoHelper.UUID_PATTERN.matcher(resource).matches()) {
+						readableIdentities[i] = translateType(activity, type) + "  (" + name + ")";
+					} else {
+						readableIdentities[i] = translateType(activity, type) + " (" + name + " / " + resource + ")";
+					}
+				} else {
+					readableIdentities[i] = translateType(activity, type) + " (" + resource + ")";
+				}
+			} else {
+				readableIdentities[i] = resource;
+			}
+		}
+		builder.setSingleChoiceItems(readableIdentities,
+				selectedResource.get(),
+				(dialog, which) -> selectedResource.set(which));
+		builder.setNegativeButton(R.string.cancel, null);
+		builder.setPositiveButton(R.string.ok, (dialog, which) -> {
+			try {
+				Jid next = Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), resourceArray[selectedResource.get()]);
+				conversation.setNextCounterpart(next);
+			} catch (InvalidJidException e) {
+				conversation.setNextCounterpart(null);
+			}
+			listener.onPresenceSelected();
+		});
+		builder.create().show();
+	}
+
+	public static void warnMutualPresenceSubscription(Activity activity, final Conversation conversation, final OnPresenceSelected listener) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+		builder.setTitle(conversation.getContact().getJid().toString());
+		builder.setMessage(R.string.without_mutual_presence_updates);
+		builder.setNegativeButton(R.string.cancel, null);
+		builder.setPositiveButton(R.string.ignore, (dialog, which) -> {
+			conversation.setNextCounterpart(null);
+			if (listener != null) {
+				listener.onPresenceSelected();
+			}
+		});
+		builder.create().show();
+	}
+
+	private static String translateType(Context context, String type) {
+		switch (type.toLowerCase()) {
+			case "pc":
+				return context.getString(R.string.type_pc);
+			case "phone":
+				return context.getString(R.string.type_phone);
+			case "tablet":
+				return context.getString(R.string.type_tablet);
+			case "web":
+				return context.getString(R.string.type_web);
+			case "console":
+				return context.getString(R.string.type_console);
+			default:
+				return type;
+		}
+	}
+
+	public interface OnPresenceSelected {
+		void onPresenceSelected();
+	}
+}

src/main/java/eu/siacs/conversations/ui/util/SendButtonAction.java 🔗

@@ -29,11 +29,11 @@
 
 package eu.siacs.conversations.ui.util;
 
-import static eu.siacs.conversations.ui.ConversationActivity.ATTACHMENT_CHOICE_CHOOSE_IMAGE;
-import static eu.siacs.conversations.ui.ConversationActivity.ATTACHMENT_CHOICE_LOCATION;
-import static eu.siacs.conversations.ui.ConversationActivity.ATTACHMENT_CHOICE_RECORD_VIDEO;
-import static eu.siacs.conversations.ui.ConversationActivity.ATTACHMENT_CHOICE_RECORD_VOICE;
-import static eu.siacs.conversations.ui.ConversationActivity.ATTACHMENT_CHOICE_TAKE_PHOTO;
+import static eu.siacs.conversations.ui.ConversationFragment.ATTACHMENT_CHOICE_CHOOSE_IMAGE;
+import static eu.siacs.conversations.ui.ConversationFragment.ATTACHMENT_CHOICE_LOCATION;
+import static eu.siacs.conversations.ui.ConversationFragment.ATTACHMENT_CHOICE_RECORD_VIDEO;
+import static eu.siacs.conversations.ui.ConversationFragment.ATTACHMENT_CHOICE_RECORD_VOICE;
+import static eu.siacs.conversations.ui.ConversationFragment.ATTACHMENT_CHOICE_TAKE_PHOTO;
 
 public enum SendButtonAction {
 	TEXT, TAKE_PHOTO, SEND_LOCATION, RECORD_VOICE, CANCEL, CHOOSE_PICTURE, RECORD_VIDEO;

src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java 🔗

@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2018, Daniel Gultsch All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package eu.siacs.conversations.ui.util;
+
+import android.app.Activity;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.preference.PreferenceManager;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.ui.ConversationActivity;
+import eu.siacs.conversations.ui.ConversationFragment;
+import eu.siacs.conversations.utils.UIHelper;
+
+public class SendButtonTool {
+
+	public static SendButtonAction getAction(Activity activity, Conversation c, String text) {
+		final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
+		final boolean empty = text.length() == 0;
+		final boolean conference = c.getMode() == Conversation.MODE_MULTI;
+		if (c.getCorrectingMessage() != null && (empty || text.equals(c.getCorrectingMessage().getBody()))) {
+			return SendButtonAction.CANCEL;
+		} else if (conference && !c.getAccount().httpUploadAvailable()) {
+			if (empty && c.getNextCounterpart() != null) {
+				return SendButtonAction.CANCEL;
+			} else {
+				return SendButtonAction.TEXT;
+			}
+		} else {
+			if (empty) {
+				if (conference && c.getNextCounterpart() != null) {
+					return SendButtonAction.CANCEL;
+				} else {
+					String setting = preferences.getString("quick_action", activity.getResources().getString(R.string.quick_action));
+					if (!setting.equals("none") && UIHelper.receivedLocationQuestion(c.getLatestMessage())) {
+						return SendButtonAction.SEND_LOCATION;
+					} else {
+						if (setting.equals("recent")) {
+							setting = preferences.getString(ConversationFragment.RECENTLY_USED_QUICK_ACTION, SendButtonAction.TEXT.toString());
+							return SendButtonAction.valueOfOrDefault(setting, SendButtonAction.TEXT);
+						} else {
+							return SendButtonAction.valueOfOrDefault(setting, SendButtonAction.TEXT);
+						}
+					}
+				}
+			} else {
+				return SendButtonAction.TEXT;
+			}
+		}
+	}
+
+	public static int getSendButtonImageResource(Activity activity, SendButtonAction action, Presence.Status status) {
+		switch (action) {
+			case TEXT:
+				switch (status) {
+					case CHAT:
+					case ONLINE:
+						return R.drawable.ic_send_text_online;
+					case AWAY:
+						return R.drawable.ic_send_text_away;
+					case XA:
+					case DND:
+						return R.drawable.ic_send_text_dnd;
+					default:
+						return getThemeResource(activity, R.attr.ic_send_text_offline, R.drawable.ic_send_text_offline);
+				}
+			case RECORD_VIDEO:
+				switch (status) {
+					case CHAT:
+					case ONLINE:
+						return R.drawable.ic_send_videocam_online;
+					case AWAY:
+						return R.drawable.ic_send_videocam_away;
+					case XA:
+					case DND:
+						return R.drawable.ic_send_videocam_dnd;
+					default:
+						return getThemeResource(activity, R.attr.ic_send_videocam_offline, R.drawable.ic_send_videocam_offline);
+				}
+			case TAKE_PHOTO:
+				switch (status) {
+					case CHAT:
+					case ONLINE:
+						return R.drawable.ic_send_photo_online;
+					case AWAY:
+						return R.drawable.ic_send_photo_away;
+					case XA:
+					case DND:
+						return R.drawable.ic_send_photo_dnd;
+					default:
+						return getThemeResource(activity, R.attr.ic_send_photo_offline, R.drawable.ic_send_photo_offline);
+				}
+			case RECORD_VOICE:
+				switch (status) {
+					case CHAT:
+					case ONLINE:
+						return R.drawable.ic_send_voice_online;
+					case AWAY:
+						return R.drawable.ic_send_voice_away;
+					case XA:
+					case DND:
+						return R.drawable.ic_send_voice_dnd;
+					default:
+						return getThemeResource(activity, R.attr.ic_send_voice_offline, R.drawable.ic_send_voice_offline);
+				}
+			case SEND_LOCATION:
+				switch (status) {
+					case CHAT:
+					case ONLINE:
+						return R.drawable.ic_send_location_online;
+					case AWAY:
+						return R.drawable.ic_send_location_away;
+					case XA:
+					case DND:
+						return R.drawable.ic_send_location_dnd;
+					default:
+						return getThemeResource(activity, R.attr.ic_send_location_offline, R.drawable.ic_send_location_offline);
+				}
+			case CANCEL:
+				switch (status) {
+					case CHAT:
+					case ONLINE:
+						return R.drawable.ic_send_cancel_online;
+					case AWAY:
+						return R.drawable.ic_send_cancel_away;
+					case XA:
+					case DND:
+						return R.drawable.ic_send_cancel_dnd;
+					default:
+						return getThemeResource(activity, R.attr.ic_send_cancel_offline, R.drawable.ic_send_cancel_offline);
+				}
+			case CHOOSE_PICTURE:
+				switch (status) {
+					case CHAT:
+					case ONLINE:
+						return R.drawable.ic_send_picture_online;
+					case AWAY:
+						return R.drawable.ic_send_picture_away;
+					case XA:
+					case DND:
+						return R.drawable.ic_send_picture_dnd;
+					default:
+						return getThemeResource(activity, R.attr.ic_send_picture_offline, R.drawable.ic_send_picture_offline);
+				}
+		}
+		return getThemeResource(activity, R.attr.ic_send_text_offline, R.drawable.ic_send_text_offline);
+	}
+
+	private static int getThemeResource(Activity activity, int r_attr_name, int r_drawable_def) {
+		int[] attrs = {r_attr_name};
+		TypedArray ta = activity.getTheme().obtainStyledAttributes(attrs);
+
+		int res = ta.getResourceId(0, r_drawable_def);
+		ta.recycle();
+
+		return res;
+	}
+
+}

src/main/java/eu/siacs/conversations/utils/UIHelper.java 🔗

@@ -532,21 +532,4 @@ public class UIHelper {
 				return new ListItem.Tag(context.getString(R.string.presence_online), 0xff259b24);
 		}
 	}
-
-	public static String tranlasteType(Context context, String type) {
-		switch (type.toLowerCase()) {
-			case "pc":
-				return context.getString(R.string.type_pc);
-			case "phone":
-				return context.getString(R.string.type_phone);
-			case "tablet":
-				return context.getString(R.string.type_tablet);
-			case "web":
-				return context.getString(R.string.type_web);
-			case "console":
-				return context.getString(R.string.type_console);
-			default:
-				return type;
-		}
-	}
 }

src/main/res/menu/activity_conversations.xml 🔗

@@ -0,0 +1,22 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/action_add"
+        android:icon="?attr/icon_new"
+        android:orderInCategory="10"
+        app:showAsAction="always"
+        android:title="@string/action_add"/>
+
+    <item
+        android:id="@+id/action_accounts"
+        android:orderInCategory="90"
+        app:showAsAction="never"
+        android:title="@string/action_accounts"/>
+    <item
+        android:id="@+id/action_settings"
+        android:orderInCategory="100"
+        app:showAsAction="never"
+        android:title="@string/action_settings"/>
+
+</menu>

src/main/res/menu/conversations.xml → src/main/res/menu/fragment_conversation.xml 🔗

@@ -1,12 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <item
-        android:id="@+id/action_add"
-        android:icon="?attr/icon_new"
-        android:orderInCategory="10"
-        app:showAsAction="always"
-        android:title="@string/action_add"/>
+      xmlns:app="http://schemas.android.com/apk/res-auto">
     <item
         android:id="@+id/action_security"
         android:icon="?attr/icon_not_secure"
@@ -103,16 +97,4 @@
         android:orderInCategory="71"
         app:showAsAction="never"
         android:title="@string/enable_notifications"/>
-
-    <item
-        android:id="@+id/action_accounts"
-        android:orderInCategory="90"
-        app:showAsAction="never"
-        android:title="@string/action_accounts"/>
-    <item
-        android:id="@+id/action_settings"
-        android:orderInCategory="100"
-        app:showAsAction="never"
-        android:title="@string/action_settings"/>
-
 </menu>