/** * 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.SMP; 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.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.provider.ContactsContract; import android.support.annotation.NonNull; 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.Toast; import com.afollestad.materialdialogs.AlertDialogWrapper; import com.google.protobuf.ByteString; import org.thoughtcrime.SMP.TransportOptions.OnTransportChangedListener; import org.thoughtcrime.SMP.components.AnimatingToggle; import org.thoughtcrime.SMP.components.ComposeText; import org.thoughtcrime.SMP.components.SendButton; import org.thoughtcrime.SMP.components.emoji.EmojiDrawer; import org.thoughtcrime.SMP.components.emoji.EmojiToggle; import org.thoughtcrime.SMP.contacts.ContactAccessor; import org.thoughtcrime.SMP.contacts.ContactAccessor.ContactData; import org.thoughtcrime.SMP.crypto.MasterCipher; import org.thoughtcrime.SMP.crypto.MasterSecret; import org.thoughtcrime.SMP.crypto.SMP.SMPSyncContentObserver; import org.thoughtcrime.SMP.crypto.SecurityEvent; import org.thoughtcrime.SMP.database.DatabaseFactory; import org.thoughtcrime.SMP.database.DraftDatabase; import org.thoughtcrime.SMP.database.DraftDatabase.Draft; import org.thoughtcrime.SMP.database.DraftDatabase.Drafts; import org.thoughtcrime.SMP.database.GroupDatabase; import org.thoughtcrime.SMP.database.MmsSmsColumns.Types; import org.thoughtcrime.SMP.database.ThreadDatabase; import org.thoughtcrime.SMP.mms.AttachmentManager; import org.thoughtcrime.SMP.mms.AttachmentTypeSelectorAdapter; import org.thoughtcrime.SMP.mms.MediaTooLargeException; import org.thoughtcrime.SMP.mms.MmsMediaConstraints; import org.thoughtcrime.SMP.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.SMP.mms.OutgoingMediaMessage; import org.thoughtcrime.SMP.mms.OutgoingSecureMediaMessage; import org.thoughtcrime.SMP.mms.Slide; import org.thoughtcrime.SMP.mms.SlideDeck; import org.thoughtcrime.SMP.notifications.MessageNotifier; import org.thoughtcrime.SMP.recipients.Recipient; import org.thoughtcrime.SMP.recipients.RecipientFactory; import org.thoughtcrime.SMP.recipients.RecipientFormattingException; import org.thoughtcrime.SMP.recipients.Recipients; import org.thoughtcrime.SMP.recipients.Recipients.RecipientsModifiedListener; import org.thoughtcrime.SMP.service.KeyCachingService; import org.thoughtcrime.SMP.sms.MessageSender; import org.thoughtcrime.SMP.sms.OutgoingEncryptedMessage; import org.thoughtcrime.SMP.sms.OutgoingEndSessionMessage; import org.thoughtcrime.SMP.sms.OutgoingTextMessage; import org.thoughtcrime.SMP.util.BitmapDecodingException; import org.thoughtcrime.SMP.util.CharacterCalculator.CharacterState; import org.thoughtcrime.SMP.util.Dialogs; import org.thoughtcrime.SMP.util.DirectoryHelper; import org.thoughtcrime.SMP.util.DynamicLanguage; import org.thoughtcrime.SMP.util.DynamicTheme; import org.thoughtcrime.SMP.util.GroupUtil; import org.thoughtcrime.SMP.util.TextSecurePreferences; import org.thoughtcrime.SMP.util.Util; 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.SMP.TransportOption.Type; import static org.thoughtcrime.SMP.database.GroupDatabase.GroupRecord; import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.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 { 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 CAPTURE_PHOTO = 6; private MasterSecret masterSecret; private ComposeText composeText; private AnimatingToggle buttonToggle; private SendButton sendButton; private ImageButton attachButton; private ConversationTitleView titleView; private TextView charactersLeft; private ConversationFragment fragment; private Button unblockButton; private View composePanel; private AttachmentTypeSelectorAdapter attachmentAdapter; private AttachmentManager attachmentManager; private BroadcastReceiver securityUpdateReceiver; private BroadcastReceiver groupUpdateReceiver; private Optional<EmojiDrawer> emojiDrawer = Optional.absent(); private EmojiToggle emojiToggle; 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(); Handler handler; String CONVERSATION_URI = "content://textsecure/smpSync/thread/"; SMPSyncContentObserver smpSyncContentObserver; @Override protected void onPreCreate() { dynamicTheme.onCreate(this); dynamicLanguage.onCreate(this); overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out); } @Override protected void onCreate(Bundle state, @NonNull MasterSecret masterSecret) { this.masterSecret = masterSecret; setContentView(R.layout.conversation_activity); fragment = initFragment(R.id.fragment_content, new ConversationFragment(), masterSecret, dynamicLanguage.getCurrentLocale()); initializeReceivers(); initializeActionBar(); initializeViews(); initializeResources(); initializeDraft(); // TODO: build content observer here to listen to incoming SMP messages // TODO: ContentObserver handler = new Handler(Looper.getMainLooper()); smpSyncContentObserver = new SMPSyncContentObserver(handler, this, recipients); getContentResolver().registerContentObserver(Uri.parse (CONVERSATION_URI + threadId /*+ "/smpSync"*/), true, smpSyncContentObserver); } @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); initializeSecurity(); initializeEnabledCheck(); initializeMmsEnabledCheck(); initializeIme(); titleView.setTitle(recipients); 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); getContentResolver().unregisterContentObserver(smpSyncContentObserver); } @Override protected void onDestroy() { saveDraft(); if (recipients != null) recipients.removeListener(this); if (securityUpdateReceiver != null) unregisterReceiver(securityUpdateReceiver); if (groupUpdateReceiver != null) unregisterReceiver(groupUpdateReceiver); 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 != CAPTURE_PHOTO) || resultCode != RESULT_OK) return; switch (reqCode) { case PICK_IMAGE: addAttachmentImage(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 CAPTURE_PHOTO: if (attachmentManager.getCaptureFile() != null) { addAttachmentImage(Uri.fromFile(attachmentManager.getCaptureFile())); } break; case GROUP_EDIT: this.recipients = RecipientFactory.getRecipientsForIds(this, data.getLongArrayExtra(GroupCreateActivity.GROUP_RECIPIENT_EXTRA), true); titleView.setTitle(recipients); setBlockedUserState(recipients); supportInvalidateOptionsMenu(); 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 android.R.id.home: handleReturnToConversationList(); return true; } return false; } @Override public void onBackPressed() { if (isEmojiDrawerOpen()) { getEmojiDrawer().hide(); emojiToggle.toggle(); } else { super.onBackPressed(); } } //////// 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 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()); // TODO: keep?! verifyIdentityIntent.putExtra("initiator", true); 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(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(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); } } private void initializeViews() { 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); 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()); } }); 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()); startActivity(intent); } }); 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()); } private void initializeActionBar() { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setCustomView(R.layout.conversation_title_view); getSupportActionBar().setDisplayShowCustomEnabled(true); getSupportActionBar().setDisplayShowTitleEnabled(false); } private EmojiDrawer getEmojiDrawer() { if (emojiDrawer.isPresent()) return emojiDrawer.get(); EmojiDrawer emojiDrawer = (EmojiDrawer)((ViewStub)findViewById(R.id.emoji_drawer_stub)).inflate(); emojiDrawer.setComposeEditText(composeText); this.emojiDrawer = Optional.of(emojiDrawer); return emojiDrawer; } private boolean isEmojiDrawerOpen() { return emojiDrawer.isPresent() && emojiDrawer.get().isOpen(); } 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); } }); } 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.TAKE_PHOTO: attachmentManager.capturePhoto(this, CAPTURE_PHOTO); break; 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; } } private void addAttachmentImage(Uri imageUri) { try { attachmentManager.setImage(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; } private void saveDraft() { if (this.recipients == null || this.recipients.isEmpty()) return; final Drafts drafts = getDraftsForCurrentState(); final long thisThreadId = this.threadId; final MasterSecret thisMasterSecret = this.masterSecret.parcelClone(); final int thisDistributionType = this.distributionType; new AsyncTask<Long, Void, Void>() { @Override protected Void 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 null; } }.execute(thisThreadId); } 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(); } private Recipients getRecipients() { return this.recipients; } 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); } private 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); } else { buttonToggle.display(sendButton); } } // Listeners private class AttachmentTypeListener implements DialogInterface.OnClickListener { @Override public void onClick(DialogInterface dialog, int which) { addAttachment(attachmentAdapter.buttonToCommand(which)); dialog.dismiss(); } } private class EmojiToggleListener implements OnClickListener { @Override public void onClick(View v) { InputMethodManager input = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); if (isEmojiDrawerOpen()) { input.showSoftInput(composeText, 0); getEmojiDrawer().hide(); } else { input.hideSoftInputFromWindow(composeText.getWindowToken(), 0); getEmojiDrawer().show(); } } } 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) { if (isEmojiDrawerOpen()) { emojiToggle.performClick(); } } @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()) { emojiToggle.performClick(); } } } @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(); } }