org.thoughtcrime.securesms.ConversationActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.thoughtcrime.securesms.ConversationActivity.java

Source

/**
 * Copyright (C) 2011 Whisper Systems
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.thoughtcrime.securesms;

import android.annotation.TargetApi;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.PorterDuff.Mode;
import android.graphics.drawable.ColorDrawable;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.v4.view.WindowCompat;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.view.View.OnKeyListener;
import android.view.ViewStub;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;

import com.afollestad.materialdialogs.AlertDialogWrapper;
import com.commonsware.cwac.camera.CameraHost.FailureReason;
import com.google.protobuf.ByteString;

import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.components.ComposeText;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
import org.thoughtcrime.securesms.components.SendButton;
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer.EmojiEventListener;
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.components.camera.HidingImageButton;
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.AttachmentDrawerListener;
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter;
import org.thoughtcrime.securesms.mms.MediaTooLargeException;
import org.thoughtcrime.securesms.mms.MmsMediaConstraints;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.providers.CaptureProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.recipients.Recipients.RecipientsModifiedListener;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.util.guava.Optional;

import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.List;

import static org.thoughtcrime.securesms.TransportOption.Type;
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import static org.whispersystems.textsecure.internal.push.TextSecureProtos.GroupContext;

/**
 * Activity for displaying a message thread, as well as
 * composing/sending a new message into that thread.
 *
 * @author Moxie Marlinspike
 *
 */
public class ConversationActivity extends PassphraseRequiredActionBarActivity
        implements ConversationFragment.ConversationFragmentListener, AttachmentManager.AttachmentListener,
        RecipientsModifiedListener, OnKeyboardShownListener, AttachmentDrawerListener {
    private static final String TAG = ConversationActivity.class.getSimpleName();

    public static final String RECIPIENTS_EXTRA = "recipients";
    public static final String THREAD_ID_EXTRA = "thread_id";
    public static final String DRAFT_TEXT_EXTRA = "draft_text";
    public static final String DRAFT_IMAGE_EXTRA = "draft_image";
    public static final String DRAFT_AUDIO_EXTRA = "draft_audio";
    public static final String DRAFT_VIDEO_EXTRA = "draft_video";
    public static final String DISTRIBUTION_TYPE_EXTRA = "distribution_type";

    private static final int PICK_IMAGE = 1;
    private static final int PICK_VIDEO = 2;
    private static final int PICK_AUDIO = 3;
    private static final int PICK_CONTACT_INFO = 4;
    private static final int GROUP_EDIT = 5;
    private static final int TAKE_PHOTO = 6;

    private MasterSecret masterSecret;
    protected ComposeText composeText;
    private AnimatingToggle buttonToggle;
    private SendButton sendButton;
    private ImageButton attachButton;
    protected ConversationTitleView titleView;
    private TextView charactersLeft;
    private ConversationFragment fragment;
    private Button unblockButton;
    private KeyboardAwareLinearLayout container;
    private View composePanel;
    private View composeBubble;

    private AttachmentTypeSelectorAdapter attachmentAdapter;
    private AttachmentManager attachmentManager;
    private BroadcastReceiver securityUpdateReceiver;
    private BroadcastReceiver groupUpdateReceiver;
    private Optional<EmojiDrawer> emojiDrawer = Optional.absent();
    private EmojiToggle emojiToggle;
    protected HidingImageButton quickAttachmentToggle;
    private QuickAttachmentDrawer quickAttachmentDrawer;

    private Recipients recipients;
    private long threadId;
    private int distributionType;
    private boolean isEncryptedConversation;
    private boolean isMmsEnabled = true;

    private DynamicTheme dynamicTheme = new DynamicTheme();
    private DynamicLanguage dynamicLanguage = new DynamicLanguage();

    @TargetApi(Build.VERSION_CODES.KITKAT)
    @Override
    protected void onPreCreate() {
        dynamicTheme.onCreate(this);
        dynamicLanguage.onCreate(this);
    }

    @Override
    protected void onCreate(Bundle state, @NonNull MasterSecret masterSecret) {
        this.masterSecret = masterSecret;

        supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY);
        setContentView(R.layout.conversation_activity);

        fragment = initFragment(R.id.fragment_content, new ConversationFragment(), masterSecret,
                dynamicLanguage.getCurrentLocale());

        initializeReceivers();
        initializeActionBar();
        initializeViews();
        initializeResources();
        initializeDraft();
    }

    @Override
    protected void onNewIntent(Intent intent) {
        Log.w(TAG, "onNewIntent()");

        if (!Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent()) {
            saveDraft();
            attachmentManager.clear();
            composeText.setText("");
        }

        setIntent(intent);
        initializeResources();
        initializeDraft();

        if (fragment != null) {
            fragment.onNewIntent();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        dynamicTheme.onResume(this);
        dynamicLanguage.onResume(this);
        quickAttachmentDrawer.onResume();

        initializeSecurity();
        initializeEnabledCheck();
        initializeMmsEnabledCheck();
        initializeIme();

        titleView.setTitle(recipients);
        setActionBarColor(recipients.getColor());
        setBlockedUserState(recipients);
        calculateCharactersRemaining();

        MessageNotifier.setVisibleThread(threadId);
        markThreadAsRead();
    }

    @Override
    protected void onPause() {
        super.onPause();
        MessageNotifier.setVisibleThread(-1L);
        if (isFinishing())
            overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
        quickAttachmentDrawer.onPause();
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        quickAttachmentDrawer.onConfigurationChanged();
        hideEmojiDrawer(false);
    }

    @Override
    protected void onDestroy() {
        saveDraft();
        if (recipients != null)
            recipients.removeListener(this);
        if (securityUpdateReceiver != null)
            unregisterReceiver(securityUpdateReceiver);
        if (groupUpdateReceiver != null)
            unregisterReceiver(groupUpdateReceiver);
        if (isEmojiDrawerOpen())
            hideEmojiDrawer(false);
        super.onDestroy();
    }

    @Override
    public void onActivityResult(int reqCode, int resultCode, Intent data) {
        Log.w(TAG, "onActivityResult called: " + reqCode + ", " + resultCode + " , " + data);
        super.onActivityResult(reqCode, resultCode, data);

        if (data == null && reqCode != TAKE_PHOTO || resultCode != RESULT_OK)
            return;

        switch (reqCode) {
        case PICK_IMAGE:
            addAttachmentImage(masterSecret, data.getData());
            break;
        case PICK_VIDEO:
            addAttachmentVideo(data.getData());
            break;
        case PICK_AUDIO:
            addAttachmentAudio(data.getData());
            break;
        case PICK_CONTACT_INFO:
            addAttachmentContactInfo(data.getData());
            break;
        case GROUP_EDIT:
            this.recipients = RecipientFactory.getRecipientsForIds(this,
                    data.getLongArrayExtra(GroupCreateActivity.GROUP_RECIPIENT_EXTRA), true);
            titleView.setTitle(recipients);
            setBlockedUserState(recipients);
            supportInvalidateOptionsMenu();
            break;
        case TAKE_PHOTO:
            if (attachmentManager.getCaptureUri() != null) {
                addAttachmentImage(masterSecret, attachmentManager.getCaptureUri());
            }
            break;
        }
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        MenuInflater inflater = this.getMenuInflater();
        menu.clear();

        if (isSingleConversation() && isEncryptedConversation) {
            inflater.inflate(R.menu.conversation_secure_identity, menu);
            inflater.inflate(R.menu.conversation_secure_sms, menu.findItem(R.id.menu_security).getSubMenu());
        } else if (isSingleConversation()) {
            inflater.inflate(R.menu.conversation_insecure, menu);
        }

        if (isSingleConversation()) {
            inflater.inflate(R.menu.conversation_callable, menu);
        } else if (isGroupConversation()) {
            inflater.inflate(R.menu.conversation_group_options, menu);

            if (!isPushGroupConversation()) {
                inflater.inflate(R.menu.conversation_mms_group_options, menu);
                if (distributionType == ThreadDatabase.DistributionTypes.BROADCAST) {
                    menu.findItem(R.id.menu_distribution_broadcast).setChecked(true);
                } else {
                    menu.findItem(R.id.menu_distribution_conversation).setChecked(true);
                }
            } else if (isActiveGroup()) {
                inflater.inflate(R.menu.conversation_push_group_options, menu);
            }
        }

        inflater.inflate(R.menu.conversation, menu);

        if (recipients != null && recipients.isMuted())
            inflater.inflate(R.menu.conversation_muted, menu);
        else
            inflater.inflate(R.menu.conversation_unmuted, menu);

        if (isSingleConversation() && getRecipients().getPrimaryRecipient().getContactUri() == null) {
            inflater.inflate(R.menu.conversation_add_to_contacts, menu);
        }

        super.onPrepareOptionsMenu(menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        super.onOptionsItemSelected(item);
        switch (item.getItemId()) {
        case R.id.menu_call:
            handleDial(getRecipients().getPrimaryRecipient());
            return true;
        case R.id.menu_delete_thread:
            handleDeleteThread();
            return true;
        case R.id.menu_add_attachment:
            handleAddAttachment();
            return true;
        case R.id.menu_view_media:
            handleViewMedia();
            return true;
        case R.id.menu_add_to_contacts:
            handleAddToContacts();
            return true;
        case R.id.menu_abort_session:
            handleAbortSecureSession();
            return true;
        case R.id.menu_verify_identity:
            handleVerifyIdentity();
            return true;
        case R.id.menu_group_recipients:
            handleDisplayGroupRecipients();
            return true;
        case R.id.menu_distribution_broadcast:
            handleDistributionBroadcastEnabled(item);
            return true;
        case R.id.menu_distribution_conversation:
            handleDistributionConversationEnabled(item);
            return true;
        case R.id.menu_edit_group:
            handleEditPushGroup();
            return true;
        case R.id.menu_leave:
            handleLeavePushGroup();
            return true;
        case R.id.menu_invite:
            handleInviteLink();
            return true;
        case R.id.menu_mute_notifications:
            handleMuteNotifications();
            return true;
        case R.id.menu_unmute_notifications:
            handleUnmuteNotifications();
            return true;
        case R.id.menu_conversation_settings:
            handleConversationSettings();
            return true;
        case android.R.id.home:
            handleReturnToConversationList();
            return true;
        }

        return false;
    }

    @Override
    public void onBackPressed() {
        Log.w(TAG, "onBackPressed()");
        if (isEmojiDrawerOpen()) {
            Log.w(TAG, "hiding emoji popup");
            hideEmojiDrawer(false);
        } else if (quickAttachmentDrawer.isOpen()) {
            quickAttachmentDrawer.close();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public void onKeyboardShown() {
        hideEmojiDrawer(true);
    }

    //////// Event Handlers

    private void handleReturnToConversationList() {
        Intent intent = new Intent(this, ConversationListActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        startActivity(intent);
        finish();
    }

    private void handleMuteNotifications() {
        MuteDialog.show(this, new MuteDialog.MuteSelectionListener() {
            @Override
            public void onMuted(final long until) {
                recipients.setMuted(until);

                new AsyncTask<Void, Void, Void>() {
                    @Override
                    protected Void doInBackground(Void... params) {
                        DatabaseFactory.getRecipientPreferenceDatabase(ConversationActivity.this)
                                .setMuted(recipients, until);

                        return null;
                    }
                }.execute();
            }
        });
    }

    private void handleConversationSettings() {
        titleView.performClick();
    }

    private void handleUnmuteNotifications() {
        recipients.setMuted(0);

        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                DatabaseFactory.getRecipientPreferenceDatabase(ConversationActivity.this).setMuted(recipients, 0);

                return null;
            }
        }.execute();
    }

    private void handleUnblock() {
        new AlertDialogWrapper.Builder(this).setTitle(R.string.ConversationActivity_unblock_question)
                .setMessage(R.string.ConversationActivity_are_you_sure_you_want_to_unblock_this_contact)
                .setNegativeButton(android.R.string.cancel, null)
                .setPositiveButton(R.string.ConversationActivity_unblock, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        recipients.setBlocked(false);

                        new AsyncTask<Void, Void, Void>() {
                            @Override
                            protected Void doInBackground(Void... params) {
                                DatabaseFactory.getRecipientPreferenceDatabase(ConversationActivity.this)
                                        .setBlocked(recipients, false);
                                return null;
                            }
                        }.execute();
                    }
                }).show();
    }

    private void handleInviteLink() {
        try {
            boolean a = SecureRandom.getInstance("SHA1PRNG").nextBoolean();
            if (a)
                composeText.appendInvite(
                        getString(R.string.ConversationActivity_get_with_it, "http://sgnl.link/1IvurmD"));
            else
                composeText.appendInvite(
                        getString(R.string.ConversationActivity_lets_use_this_to_chat, "http://sgnl.link/1CYCQQN"));
        } catch (NoSuchAlgorithmException e) {
            throw new AssertionError(e);
        }
    }

    private void handleVerifyIdentity() {
        Intent verifyIdentityIntent = new Intent(this, VerifyIdentityActivity.class);
        verifyIdentityIntent.putExtra("recipient", getRecipients().getPrimaryRecipient().getRecipientId());
        startActivity(verifyIdentityIntent);
    }

    private void handleAbortSecureSession() {
        AlertDialogWrapper.Builder builder = new AlertDialogWrapper.Builder(this);
        builder.setTitle(R.string.ConversationActivity_abort_secure_session_confirmation);
        builder.setIconAttribute(R.attr.dialog_alert_icon);
        builder.setCancelable(true);
        builder.setMessage(
                R.string.ConversationActivity_are_you_sure_that_you_want_to_abort_this_secure_session_question);
        builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                if (isSingleConversation()) {
                    final Context context = getApplicationContext();

                    OutgoingEndSessionMessage endSessionMessage = new OutgoingEndSessionMessage(
                            new OutgoingTextMessage(getRecipients(), "TERMINATE"));

                    new AsyncTask<OutgoingEndSessionMessage, Void, Long>() {
                        @Override
                        protected Long doInBackground(OutgoingEndSessionMessage... messages) {
                            return MessageSender.send(context, masterSecret, messages[0], threadId, false);
                        }

                        @Override
                        protected void onPostExecute(Long result) {
                            sendComplete(result);
                        }
                    }.execute(endSessionMessage);
                }
            }
        });
        builder.setNegativeButton(R.string.no, null);
        builder.show();
    }

    private void handleViewMedia() {
        Intent intent = new Intent(this, MediaOverviewActivity.class);
        intent.putExtra(MediaOverviewActivity.THREAD_ID_EXTRA, threadId);
        intent.putExtra(MediaOverviewActivity.RECIPIENT_EXTRA, recipients.getPrimaryRecipient().getRecipientId());
        startActivity(intent);
    }

    private void handleLeavePushGroup() {
        if (getRecipients() == null) {
            Toast.makeText(this, getString(R.string.ConversationActivity_invalid_recipient), Toast.LENGTH_LONG)
                    .show();
            return;
        }

        AlertDialogWrapper.Builder builder = new AlertDialogWrapper.Builder(this);
        builder.setTitle(getString(R.string.ConversationActivity_leave_group));
        builder.setIconAttribute(R.attr.dialog_info_icon);
        builder.setCancelable(true);
        builder.setMessage(getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group));
        builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Context self = ConversationActivity.this;
                try {
                    byte[] groupId = GroupUtil.getDecodedId(getRecipients().getPrimaryRecipient().getNumber());
                    DatabaseFactory.getGroupDatabase(self).setActive(groupId, false);

                    GroupContext context = GroupContext.newBuilder().setId(ByteString.copyFrom(groupId))
                            .setType(GroupContext.Type.QUIT).build();

                    OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(self, getRecipients(),
                            context, null);
                    MessageSender.send(self, masterSecret, outgoingMessage, threadId, false);
                    DatabaseFactory.getGroupDatabase(self).remove(groupId,
                            TextSecurePreferences.getLocalNumber(self));
                    initializeEnabledCheck();
                } catch (IOException e) {
                    Log.w(TAG, e);
                    Toast.makeText(self, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG)
                            .show();
                }
            }
        });

        builder.setNegativeButton(R.string.no, null);
        builder.show();
    }

    private void handleEditPushGroup() {
        Intent intent = new Intent(ConversationActivity.this, GroupCreateActivity.class);
        intent.putExtra(GroupCreateActivity.GROUP_RECIPIENT_EXTRA,
                recipients.getPrimaryRecipient().getRecipientId());
        startActivityForResult(intent, GROUP_EDIT);
    }

    private void handleDistributionBroadcastEnabled(MenuItem item) {
        distributionType = ThreadDatabase.DistributionTypes.BROADCAST;
        item.setChecked(true);

        if (threadId != -1) {
            new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... params) {
                    DatabaseFactory.getThreadDatabase(ConversationActivity.this).setDistributionType(threadId,
                            ThreadDatabase.DistributionTypes.BROADCAST);
                    return null;
                }
            }.execute();
        }
    }

    private void handleDistributionConversationEnabled(MenuItem item) {
        distributionType = ThreadDatabase.DistributionTypes.CONVERSATION;
        item.setChecked(true);

        if (threadId != -1) {
            new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... params) {
                    DatabaseFactory.getThreadDatabase(ConversationActivity.this).setDistributionType(threadId,
                            ThreadDatabase.DistributionTypes.CONVERSATION);
                    return null;
                }
            }.execute();
        }
    }

    private void handleDial(Recipient recipient) {
        try {
            if (recipient == null)
                return;

            Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + recipient.getNumber()));
            startActivity(dialIntent);
        } catch (ActivityNotFoundException anfe) {
            Log.w(TAG, anfe);
            Dialogs.showAlertDialog(this, getString(R.string.ConversationActivity_calls_not_supported),
                    getString(R.string.ConversationActivity_this_device_does_not_appear_to_support_dial_actions));
        }
    }

    private void handleDisplayGroupRecipients() {
        new GroupMembersDialog(this, getRecipients()).display();
    }

    private void handleDeleteThread() {
        AlertDialogWrapper.Builder builder = new AlertDialogWrapper.Builder(this);
        builder.setTitle(R.string.ConversationActivity_delete_thread_confirmation);
        builder.setIconAttribute(R.attr.dialog_alert_icon);
        builder.setCancelable(true);
        builder.setMessage(
                R.string.ConversationActivity_are_you_sure_that_you_want_to_permanently_delete_this_conversation_question);
        builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                if (threadId > 0) {
                    DatabaseFactory.getThreadDatabase(ConversationActivity.this).deleteConversation(threadId);
                }
                composeText.getText().clear();
                threadId = -1;
                finish();
            }
        });

        builder.setNegativeButton(R.string.no, null);
        builder.show();
    }

    private void handleAddToContacts() {
        final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
        intent.putExtra(ContactsContract.Intents.Insert.PHONE, recipients.getPrimaryRecipient().getNumber());
        intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
        startActivity(intent);
    }

    private void handleAddAttachment() {
        if (this.isMmsEnabled || DirectoryHelper.isPushDestination(this, getRecipients())) {
            new AlertDialogWrapper.Builder(this).setAdapter(attachmentAdapter, new AttachmentTypeListener()).show();
        } else {
            handleManualMmsRequired();
        }
    }

    private void handleManualMmsRequired() {
        Toast.makeText(this, R.string.MmsDownloader_error_reading_mms_settings, Toast.LENGTH_LONG).show();

        Intent intent = new Intent(this, PromptMmsActivity.class);
        intent.putExtras(getIntent().getExtras());
        startActivity(intent);
    }

    ///// Initializers

    private void initializeDraft() {
        String draftText = getIntent().getStringExtra(DRAFT_TEXT_EXTRA);
        Uri draftImage = getIntent().getParcelableExtra(DRAFT_IMAGE_EXTRA);
        Uri draftAudio = getIntent().getParcelableExtra(DRAFT_AUDIO_EXTRA);
        Uri draftVideo = getIntent().getParcelableExtra(DRAFT_VIDEO_EXTRA);

        if (draftText != null)
            composeText.setText(draftText);
        if (draftImage != null)
            addAttachmentImage(masterSecret, draftImage);
        if (draftAudio != null)
            addAttachmentAudio(draftAudio);
        if (draftVideo != null)
            addAttachmentVideo(draftVideo);

        if (draftText == null && draftImage == null && draftAudio == null && draftVideo == null) {
            initializeDraftFromDatabase();
        } else {
            updateToggleButtonState();
        }
    }

    private void initializeEnabledCheck() {
        boolean enabled = !(isPushGroupConversation() && !isActiveGroup());
        composeText.setEnabled(enabled);
        sendButton.setEnabled(enabled);
    }

    private void initializeDraftFromDatabase() {
        new AsyncTask<Void, Void, List<Draft>>() {
            @Override
            protected List<Draft> doInBackground(Void... params) {
                MasterCipher masterCipher = new MasterCipher(masterSecret);
                DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this);
                List<Draft> results = draftDatabase.getDrafts(masterCipher, threadId);

                draftDatabase.clearDrafts(threadId);

                return results;
            }

            @Override
            protected void onPostExecute(List<Draft> drafts) {
                for (Draft draft : drafts) {
                    if (draft.getType().equals(Draft.TEXT)) {
                        composeText.setText(draft.getValue());
                    } else if (draft.getType().equals(Draft.IMAGE)) {
                        addAttachmentImage(masterSecret, Uri.parse(draft.getValue()));
                    } else if (draft.getType().equals(Draft.AUDIO)) {
                        addAttachmentAudio(Uri.parse(draft.getValue()));
                    } else if (draft.getType().equals(Draft.VIDEO)) {
                        addAttachmentVideo(Uri.parse(draft.getValue()));
                    }
                }

                updateToggleButtonState();
            }
        }.execute();
    }

    private void initializeSecurity() {
        boolean isMediaMessage = !recipients.isSingleRecipient() || attachmentManager.isAttachmentPresent();
        this.isEncryptedConversation = DirectoryHelper.isPushDestination(this, getRecipients());

        sendButton.resetAvailableTransports(isMediaMessage);

        if (!isEncryptedConversation)
            sendButton.disableTransport(Type.TEXTSECURE);
        if (recipients.isGroupRecipient())
            sendButton.disableTransport(Type.SMS);

        if (isEncryptedConversation)
            sendButton.setDefaultTransport(Type.TEXTSECURE);
        else
            sendButton.setDefaultTransport(Type.SMS);

        calculateCharactersRemaining();
        supportInvalidateOptionsMenu();
    }

    private void initializeMmsEnabledCheck() {
        new AsyncTask<Void, Void, Boolean>() {
            @Override
            protected Boolean doInBackground(Void... params) {
                return Util.isMmsCapable(ConversationActivity.this);
            }

            @Override
            protected void onPostExecute(Boolean isMmsEnabled) {
                ConversationActivity.this.isMmsEnabled = isMmsEnabled;
            }
        }.execute();
    }

    private void initializeIme() {
        if (TextSecurePreferences.isEnterSendsEnabled(this)) {
            composeText.setInputType(composeText.getInputType() & ~InputType.TYPE_TEXT_FLAG_MULTI_LINE);
            composeText.setImeOptions(composeText.getImeOptions() & ~EditorInfo.IME_FLAG_NO_ENTER_ACTION);
        } else {
            composeText.setInputType(composeText.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
            composeText.setImeOptions(composeText.getImeOptions() | EditorInfo.IME_FLAG_NO_ENTER_ACTION);
        }
        composeText.setOnEditorActionListener(new OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_SEND) {
                    sendMessage();
                    return true;
                }
                return false;
            }
        });
    }

    private void initializeViews() {
        titleView = (ConversationTitleView) getSupportActionBar().getCustomView();
        buttonToggle = (AnimatingToggle) findViewById(R.id.button_toggle);
        sendButton = (SendButton) findViewById(R.id.send_button);
        attachButton = (ImageButton) findViewById(R.id.attach_button);
        composeText = (ComposeText) findViewById(R.id.embedded_text_editor);
        charactersLeft = (TextView) findViewById(R.id.space_left);
        emojiToggle = (EmojiToggle) findViewById(R.id.emoji_toggle);
        unblockButton = (Button) findViewById(R.id.unblock_button);
        composePanel = findViewById(R.id.bottom_panel);
        composeBubble = findViewById(R.id.compose_bubble);
        container = (KeyboardAwareLinearLayout) findViewById(R.id.layout_container);

        container.addOnKeyboardShownListener(this);

        buttonToggle = (AnimatingToggle) findViewById(R.id.button_toggle);
        sendButton = (SendButton) findViewById(R.id.send_button);
        attachButton = (ImageButton) findViewById(R.id.attach_button);
        composeText = (ComposeText) findViewById(R.id.embedded_text_editor);
        charactersLeft = (TextView) findViewById(R.id.space_left);
        emojiToggle = (EmojiToggle) findViewById(R.id.emoji_toggle);
        titleView = (ConversationTitleView) getSupportActionBar().getCustomView();
        unblockButton = (Button) findViewById(R.id.unblock_button);
        composePanel = findViewById(R.id.bottom_panel);
        quickAttachmentDrawer = (QuickAttachmentDrawer) findViewById(R.id.quick_attachment_drawer);
        quickAttachmentToggle = (HidingImageButton) findViewById(R.id.quick_attachment_toggle);

        int[] attributes = new int[] { R.attr.conversation_item_bubble_background };
        TypedArray colors = obtainStyledAttributes(attributes);
        int defaultColor = colors.getColor(0, Color.WHITE);
        composeBubble.getBackground().setColorFilter(defaultColor, PorterDuff.Mode.MULTIPLY);
        colors.recycle();

        attachmentAdapter = new AttachmentTypeSelectorAdapter(this);
        attachmentManager = new AttachmentManager(this, this);

        SendButtonListener sendButtonListener = new SendButtonListener();
        ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();

        attachButton.setOnClickListener(new AttachButtonListener());
        sendButton.setOnClickListener(sendButtonListener);
        sendButton.setEnabled(true);
        sendButton.addOnTransportChangedListener(new OnTransportChangedListener() {
            @Override
            public void onChange(TransportOption newTransport) {
                calculateCharactersRemaining();
                composeText.setHint(newTransport.getComposeHint());
                composeText.setImeActionLabel(newTransport.getComposeHint(), EditorInfo.IME_ACTION_SEND);
                composeText.setInputType(composeText.getInputType());
                buttonToggle.getBackground().setColorFilter(newTransport.getBackgroundColor(), Mode.MULTIPLY);
            }
        });

        titleView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(ConversationActivity.this, RecipientPreferenceActivity.class);
                intent.putExtra(RecipientPreferenceActivity.RECIPIENTS_EXTRA, recipients.getIds());

                startActivitySceneTransition(intent, titleView.findViewById(R.id.title), "recipient_name");
            }
        });

        unblockButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                handleUnblock();
            }
        });

        composeText.setOnKeyListener(composeKeyPressedListener);
        composeText.addTextChangedListener(composeKeyPressedListener);
        composeText.setOnEditorActionListener(sendButtonListener);
        composeText.setOnClickListener(composeKeyPressedListener);
        composeText.setOnFocusChangeListener(composeKeyPressedListener);
        emojiToggle.setOnClickListener(new EmojiToggleListener());

        if (QuickAttachmentDrawer.isDeviceSupported(this)) {
            quickAttachmentDrawer.setListener(this);
            quickAttachmentToggle.setOnClickListener(new QuickAttachmentToggleListener());
        } else {
            quickAttachmentToggle.disable();
        }
    }

    protected void initializeActionBar() {
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setCustomView(R.layout.conversation_title_view);
        getSupportActionBar().setDisplayShowCustomEnabled(true);
        getSupportActionBar().setDisplayShowTitleEnabled(false);
    }

    private EmojiDrawer getEmojiDrawer() {
        if (!emojiDrawer.isPresent()) {
            EmojiDrawer emojiDrawer = (EmojiDrawer) ((ViewStub) findViewById(R.id.emoji_drawer_stub)).inflate();
            emojiDrawer.setEmojiEventListener(new EmojiEventListener() {
                @Override
                public void onKeyEvent(KeyEvent keyEvent) {
                    composeText.dispatchKeyEvent(keyEvent);
                }

                @Override
                public void onEmojiSelected(String emoji) {
                    Log.w(TAG, "onEmojiSelected()");
                    composeText.insertEmoji(emoji);
                }
            });
            this.emojiDrawer = Optional.of(emojiDrawer);
        }
        return emojiDrawer.get();
    }

    private void showEmojiDrawer() {
        getEmojiDrawer().show(container);
        emojiToggle.setToIme();
    }

    protected void hideEmojiDrawer(boolean expectingKeyboard) {
        if (isEmojiDrawerOpen()) {
            if (!expectingKeyboard || container.isLandscape()) {
                getEmojiDrawer().dismiss();
            } else {
                container.postOnKeyboardOpen(new Runnable() {
                    @Override
                    public void run() {
                        getEmojiDrawer().dismiss();
                    }
                });
            }
        }
        emojiToggle.setToEmoji();
    }

    private boolean isEmojiDrawerOpen() {
        return emojiDrawer.isPresent() && emojiDrawer.get().isShowing();
    }

    private void initializeResources() {
        recipients = RecipientFactory.getRecipientsForIds(this, getIntent().getLongArrayExtra(RECIPIENTS_EXTRA),
                true);
        threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
        distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA,
                ThreadDatabase.DistributionTypes.DEFAULT);

        recipients.addListener(this);
    }

    @Override
    public void onModified(final Recipients recipients) {
        titleView.post(new Runnable() {
            @Override
            public void run() {
                titleView.setTitle(recipients);
                setBlockedUserState(recipients);
                setActionBarColor(recipients.getColor());
            }
        });
    }

    private void initializeReceivers() {
        securityUpdateReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                long eventThreadId = intent.getLongExtra("thread_id", -1);

                if (eventThreadId == threadId || eventThreadId == -2) {
                    initializeSecurity();
                    calculateCharactersRemaining();
                }
            }
        };

        groupUpdateReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                Log.w("ConversationActivity", "Group update received...");
                if (recipients != null) {
                    long[] ids = recipients.getIds();
                    Log.w("ConversationActivity", "Looking up new recipients...");
                    recipients = RecipientFactory.getRecipientsForIds(context, ids, false);
                    titleView.setTitle(recipients);
                }
            }
        };

        registerReceiver(securityUpdateReceiver, new IntentFilter(SecurityEvent.SECURITY_UPDATE_EVENT),
                KeyCachingService.KEY_PERMISSION, null);

        registerReceiver(groupUpdateReceiver, new IntentFilter(GroupDatabase.DATABASE_UPDATE_ACTION));
    }

    //////// Helper Methods

    private void addAttachment(int type) {
        Log.w("ComposeMessageActivity", "Selected: " + type);
        switch (type) {
        case AttachmentTypeSelectorAdapter.ADD_IMAGE:
            AttachmentManager.selectImage(this, PICK_IMAGE);
            break;
        case AttachmentTypeSelectorAdapter.ADD_VIDEO:
            AttachmentManager.selectVideo(this, PICK_VIDEO);
            break;
        case AttachmentTypeSelectorAdapter.ADD_SOUND:
            AttachmentManager.selectAudio(this, PICK_AUDIO);
            break;
        case AttachmentTypeSelectorAdapter.ADD_CONTACT_INFO:
            AttachmentManager.selectContactInfo(this, PICK_CONTACT_INFO);
            break;
        case AttachmentTypeSelectorAdapter.TAKE_PHOTO:
            attachmentManager.capturePhoto(this, recipients, TAKE_PHOTO);
            break;
        }
    }

    private void addAttachmentImage(MasterSecret masterSecret, Uri imageUri) {
        try {
            attachmentManager.setImage(masterSecret, imageUri);
        } catch (IOException | BitmapDecodingException e) {
            Log.w(TAG, e);
            attachmentManager.clear();
            Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
                    Toast.LENGTH_LONG).show();
        }
    }

    private void addAttachmentVideo(Uri videoUri) {
        try {
            attachmentManager.setVideo(videoUri);
        } catch (IOException e) {
            attachmentManager.clear();
            Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
                    Toast.LENGTH_LONG).show();
            Log.w("ComposeMessageActivity", e);
        } catch (MediaTooLargeException e) {
            attachmentManager.clear();

            Toast.makeText(this, getString(
                    R.string.ConversationActivity_sorry_the_selected_video_exceeds_message_size_restrictions,
                    (MmsMediaConstraints.MAX_MESSAGE_SIZE / 1024)), Toast.LENGTH_LONG).show();
            Log.w("ComposeMessageActivity", e);
        }
    }

    private void addAttachmentAudio(Uri audioUri) {
        try {
            attachmentManager.setAudio(audioUri);
        } catch (IOException e) {
            attachmentManager.clear();
            Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
                    Toast.LENGTH_LONG).show();
            Log.w("ComposeMessageActivity", e);
        } catch (MediaTooLargeException e) {
            attachmentManager.clear();
            Toast.makeText(this, getString(
                    R.string.ConversationActivity_sorry_the_selected_audio_exceeds_message_size_restrictions,
                    (MmsMediaConstraints.MAX_MESSAGE_SIZE / 1024)), Toast.LENGTH_LONG).show();
            Log.w("ComposeMessageActivity", e);
        }
    }

    private void addAttachmentContactInfo(Uri contactUri) {
        ContactAccessor contactDataList = ContactAccessor.getInstance();
        ContactData contactData = contactDataList.getContactData(this, contactUri);

        if (contactData.numbers.size() == 1)
            composeText.append(contactData.numbers.get(0).number);
        else if (contactData.numbers.size() > 1)
            selectContactInfo(contactData);
    }

    private void selectContactInfo(ContactData contactData) {
        final CharSequence[] numbers = new CharSequence[contactData.numbers.size()];
        final CharSequence[] numberItems = new CharSequence[contactData.numbers.size()];

        for (int i = 0; i < contactData.numbers.size(); i++) {
            numbers[i] = contactData.numbers.get(i).number;
            numberItems[i] = contactData.numbers.get(i).type + ": " + contactData.numbers.get(i).number;
        }

        AlertDialogWrapper.Builder builder = new AlertDialogWrapper.Builder(this);
        builder.setIconAttribute(R.attr.conversation_attach_contact_info);
        builder.setTitle(R.string.ConversationActivity_select_contact_info);

        builder.setItems(numberItems, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                composeText.append(numbers[which]);
            }
        });
        builder.show();
    }

    private Drafts getDraftsForCurrentState() {
        Drafts drafts = new Drafts();

        if (!Util.isEmpty(composeText)) {
            drafts.add(new Draft(Draft.TEXT, composeText.getText().toString()));
        }

        for (Slide slide : attachmentManager.getSlideDeck().getSlides()) {
            if (slide.hasAudio())
                drafts.add(new Draft(Draft.AUDIO, slide.getUri().toString()));
            else if (slide.hasVideo())
                drafts.add(new Draft(Draft.VIDEO, slide.getUri().toString()));
            else if (slide.hasImage())
                drafts.add(new Draft(Draft.IMAGE, slide.getUri().toString()));
        }

        return drafts;
    }

    protected ListenableFuture<Long> saveDraft() {
        final SettableFuture<Long> future = new SettableFuture<>();

        if (this.recipients == null || this.recipients.isEmpty()) {
            future.set(threadId);
            return future;
        }

        final Drafts drafts = getDraftsForCurrentState();
        final long thisThreadId = this.threadId;
        final MasterSecret thisMasterSecret = this.masterSecret.parcelClone();
        final int thisDistributionType = this.distributionType;

        new AsyncTask<Long, Void, Long>() {
            @Override
            protected Long doInBackground(Long... params) {
                ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(ConversationActivity.this);
                DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this);
                long threadId = params[0];

                if (drafts.size() > 0) {
                    if (threadId == -1)
                        threadId = threadDatabase.getThreadIdFor(getRecipients(), thisDistributionType);

                    draftDatabase.insertDrafts(new MasterCipher(thisMasterSecret), threadId, drafts);
                    threadDatabase.updateSnippet(threadId, drafts.getSnippet(ConversationActivity.this),
                            System.currentTimeMillis(), Types.BASE_DRAFT_TYPE);
                } else if (threadId > 0) {
                    threadDatabase.update(threadId);
                }

                return threadId;
            }

            @Override
            protected void onPostExecute(Long result) {
                future.set(result);
            }

        }.execute(thisThreadId);

        return future;
    }

    private void setActionBarColor(MaterialColor color) {
        getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            getWindow().setStatusBarColor(color.toStatusBarColor(this));
        }
    }

    private void setBlockedUserState(Recipients recipients) {
        if (recipients.isBlocked()) {
            unblockButton.setVisibility(View.VISIBLE);
            composePanel.setVisibility(View.GONE);
        } else {
            composePanel.setVisibility(View.VISIBLE);
            unblockButton.setVisibility(View.GONE);
        }
    }

    private void calculateCharactersRemaining() {
        int charactersSpent = composeText.getText().toString().length();
        TransportOption transportOption = sendButton.getSelectedTransport();
        CharacterState characterState = transportOption.calculateCharacters(charactersSpent);

        if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
            charactersLeft.setText(characterState.charactersRemaining + "/" + characterState.maxMessageSize + " ("
                    + characterState.messagesSpent + ")");
            charactersLeft.setVisibility(View.VISIBLE);
        } else {
            charactersLeft.setVisibility(View.GONE);
        }
    }

    private boolean isSingleConversation() {
        return getRecipients() != null && getRecipients().isSingleRecipient()
                && !getRecipients().isGroupRecipient();
    }

    private boolean isActiveGroup() {
        if (!isGroupConversation())
            return false;

        try {
            byte[] groupId = GroupUtil.getDecodedId(getRecipients().getPrimaryRecipient().getNumber());
            GroupRecord record = DatabaseFactory.getGroupDatabase(this).getGroup(groupId);

            return record != null && record.isActive();
        } catch (IOException e) {
            Log.w("ConversationActivity", e);
            return false;
        }
    }

    private boolean isGroupConversation() {
        return getRecipients() != null
                && (!getRecipients().isSingleRecipient() || getRecipients().isGroupRecipient());
    }

    private boolean isPushGroupConversation() {
        return getRecipients() != null && getRecipients().isGroupRecipient();
    }

    protected Recipients getRecipients() {
        return this.recipients;
    }

    protected long getThreadId() {
        return this.threadId;
    }

    private String getMessage() throws InvalidMessageException {
        String rawText = composeText.getText().toString();

        if (rawText.length() < 1 && !attachmentManager.isAttachmentPresent())
            throw new InvalidMessageException(
                    getString(R.string.ConversationActivity_message_is_empty_exclamation));

        return rawText;
    }

    private void markThreadAsRead() {
        new AsyncTask<Long, Void, Void>() {
            @Override
            protected Void doInBackground(Long... params) {
                DatabaseFactory.getThreadDatabase(ConversationActivity.this).setRead(params[0]);
                MessageNotifier.updateNotification(ConversationActivity.this, masterSecret);
                return null;
            }
        }.execute(threadId);
    }

    protected void sendComplete(long threadId) {
        boolean refreshFragment = (threadId != this.threadId);
        this.threadId = threadId;

        if (fragment == null || !fragment.isVisible() || isFinishing()) {
            return;
        }

        if (refreshFragment) {
            fragment.reload(recipients, threadId);

            initializeSecurity();
        }

        fragment.scrollToBottom();
        attachmentManager.cleanup();
    }

    private void sendMessage() {
        try {
            Recipients recipients = getRecipients();
            boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();

            Log.w(TAG, "isManual Selection: " + sendButton.isManualSelection());
            Log.w(TAG, "forceSms: " + forceSms);

            if (recipients == null) {
                throw new RecipientFormattingException("Badly formatted");
            }

            if ((!recipients.isSingleRecipient() || recipients.isEmailRecipient()) && !isMmsEnabled) {
                handleManualMmsRequired();
            } else if (attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient()
                    || recipients.isGroupRecipient() || recipients.isEmailRecipient()) {
                sendMediaMessage(forceSms);
            } else {
                sendTextMessage(forceSms);
            }
        } catch (RecipientFormattingException ex) {
            Toast.makeText(ConversationActivity.this,
                    R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation,
                    Toast.LENGTH_LONG).show();
            Log.w(TAG, ex);
        } catch (InvalidMessageException ex) {
            Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_message_is_empty_exclamation,
                    Toast.LENGTH_SHORT).show();
            Log.w(TAG, ex);
        }
    }

    private void sendMediaMessage(final boolean forceSms) throws InvalidMessageException {
        final Context context = getApplicationContext();
        SlideDeck slideDeck;

        if (attachmentManager.isAttachmentPresent())
            slideDeck = new SlideDeck(attachmentManager.getSlideDeck());
        else
            slideDeck = new SlideDeck();

        OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(this, recipients, slideDeck, getMessage(),
                distributionType);

        if (isEncryptedConversation && !forceSms) {
            outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessage);
        }

        attachmentManager.clear();
        composeText.setText("");

        new AsyncTask<OutgoingMediaMessage, Void, Long>() {
            @Override
            protected Long doInBackground(OutgoingMediaMessage... messages) {
                return MessageSender.send(context, masterSecret, messages[0], threadId, forceSms);
            }

            @Override
            protected void onPostExecute(Long result) {
                sendComplete(result);
            }
        }.execute(outgoingMessage);
    }

    private void sendTextMessage(final boolean forceSms) throws InvalidMessageException {
        final Context context = getApplicationContext();
        OutgoingTextMessage message;

        if (isEncryptedConversation && !forceSms) {
            message = new OutgoingEncryptedMessage(recipients, getMessage());
        } else {
            message = new OutgoingTextMessage(recipients, getMessage());
        }

        this.composeText.setText("");

        new AsyncTask<OutgoingTextMessage, Void, Long>() {
            @Override
            protected Long doInBackground(OutgoingTextMessage... messages) {
                return MessageSender.send(context, masterSecret, messages[0], threadId, forceSms);
            }

            @Override
            protected void onPostExecute(Long result) {
                sendComplete(result);
            }
        }.execute(message);
    }

    private void updateToggleButtonState() {
        if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) {
            buttonToggle.display(attachButton);
            quickAttachmentToggle.show();
        } else {
            buttonToggle.display(sendButton);
            quickAttachmentToggle.hide();
        }
    }

    @Override
    public void onAttachmentDrawerClosed() {
        getSupportActionBar().show();
    }

    @Override
    public void onAttachmentDrawerOpened() {
        getSupportActionBar().hide();
    }

    @Override
    public void onImageCapture(@NonNull final byte[] imageBytes) {
        attachmentManager
                .setCaptureUri(CaptureProvider.getInstance(this).create(masterSecret, recipients, imageBytes));
        addAttachmentImage(masterSecret, attachmentManager.getCaptureUri());
        quickAttachmentDrawer.close();
    }

    @Override
    public void onCameraFail(FailureReason reason) {
        Toast.makeText(this, R.string.quick_camera_unavailable, Toast.LENGTH_SHORT).show();
        quickAttachmentDrawer.close();
        quickAttachmentToggle.disable();
    }

    // Listeners

    private class AttachmentTypeListener implements DialogInterface.OnClickListener {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            addAttachment(attachmentAdapter.buttonToCommand(which));
            dialog.dismiss();
        }
    }

    private void openKeyboardForComposition() {
        composeText.post(new Runnable() {
            @Override
            public void run() {
                composeText.requestFocus();
                ServiceUtil.getInputMethodManager(ConversationActivity.this).showSoftInput(composeText, 0);
            }
        });
    }

    private void hideKeyboard() {
        ServiceUtil.getInputMethodManager(this).hideSoftInputFromWindow(composeText.getWindowToken(), 0);
    }

    private class EmojiToggleListener implements OnClickListener {
        @Override
        public void onClick(View v) {
            Log.w(TAG, "EmojiToggleListener onClick()");
            if (isEmojiDrawerOpen()) {
                hideEmojiDrawer(true);
                openKeyboardForComposition();
            } else {
                container.postOnKeyboardClose(new Runnable() {
                    @Override
                    public void run() {
                        showEmojiDrawer();
                    }
                });
                quickAttachmentDrawer.close();
                hideKeyboard();
            }
        }
    }

    private class QuickAttachmentToggleListener implements OnClickListener {
        @Override
        public void onClick(View v) {
            InputMethodManager input = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
            input.hideSoftInputFromWindow(composeText.getWindowToken(), 0);
            composeText.clearFocus();
            hideEmojiDrawer(false);
            quickAttachmentDrawer.open();
        }
    }

    private class SendButtonListener implements OnClickListener, TextView.OnEditorActionListener {
        @Override
        public void onClick(View v) {
            sendMessage();
        }

        @Override
        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
            if (actionId == EditorInfo.IME_ACTION_SEND) {
                sendButton.performClick();
                return true;
            }
            return false;
        }
    }

    private class AttachButtonListener implements OnClickListener {
        @Override
        public void onClick(View v) {
            handleAddAttachment();
        }
    }

    private class ComposeKeyPressedListener
            implements OnKeyListener, OnClickListener, TextWatcher, OnFocusChangeListener {

        int beforeLength;

        @Override
        public boolean onKey(View v, int keyCode, KeyEvent event) {
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                if (keyCode == KeyEvent.KEYCODE_ENTER) {
                    if (TextSecurePreferences.isEnterSendsEnabled(ConversationActivity.this)) {
                        sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
                        sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
                        return true;
                    }
                }
            }
            return false;
        }

        @Override
        public void onClick(View v) {
            hideEmojiDrawer(true);
        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            beforeLength = composeText.getText().length();
        }

        @Override
        public void afterTextChanged(Editable s) {
            calculateCharactersRemaining();

            if (composeText.getText().length() == 0 || beforeLength == 0) {
                composeText.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        updateToggleButtonState();
                    }
                }, 50);
            }
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
        }

        @Override
        public void onFocusChange(View v, boolean hasFocus) {
            if (hasFocus && isEmojiDrawerOpen()) {
                hideEmojiDrawer(true);
            } else if (hasFocus && quickAttachmentDrawer.isOpen()) {
                quickAttachmentDrawer.close();
            }
        }
    }

    @Override
    public void setComposeText(String text) {
        this.composeText.setText(text);
    }

    @Override
    public void setThreadId(long threadId) {
        this.threadId = threadId;
    }

    @Override
    public void onAttachmentChanged() {
        initializeSecurity();
        updateToggleButtonState();
    }
}