/* * 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 . */ package org.thoughtcrime.securesms; import android.annotation.SuppressLint; 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.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Vibrator; import android.provider.Browser; import android.provider.ContactsContract; import android.provider.Telephony; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.view.MenuItemCompat; import android.support.v4.view.WindowCompat; import android.support.v7.app.AlertDialog; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.util.Pair; 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.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.google.android.gms.location.places.ui.PlacePicker; import com.google.protobuf.ByteString; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener; import org.thoughtcrime.securesms.audio.AudioRecorder; import org.thoughtcrime.securesms.audio.AudioSlidePlayer; import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.components.AnimatingToggle; import org.thoughtcrime.securesms.components.AttachmentTypeSelector; import org.thoughtcrime.securesms.components.ComposeText; import org.thoughtcrime.securesms.components.HidingLinearLayout; import org.thoughtcrime.securesms.components.InputAwareLayout; import org.thoughtcrime.securesms.components.InputPanel; import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener; import org.thoughtcrime.securesms.components.SendButton; import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer; import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.AttachmentDrawerListener; import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.DrawerState; import org.thoughtcrime.securesms.components.emoji.EmojiDrawer; import org.thoughtcrime.securesms.components.identity.UntrustedSendDialog; import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView; import org.thoughtcrime.securesms.components.identity.UnverifiedSendDialog; import org.thoughtcrime.securesms.components.location.SignalPlace; import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder; import org.thoughtcrime.securesms.components.reminder.InviteReminder; import org.thoughtcrime.securesms.components.reminder.ReminderView; import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable; import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.SecurityEvent; import org.thoughtcrime.securesms.database.Address; 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.IdentityDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord; import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MmsSmsColumns.Types; import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.identity.IdentityRecordList; import org.thoughtcrime.securesms.events.ReminderUpdateEvent; import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.mms.AttachmentManager; import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType; import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.LocationSlide; import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage; 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.MarkReadReceiver; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.profiles.GroupShareProfileView; import org.thoughtcrime.securesms.providers.PersistentBlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.scribbles.ScribbleActivity; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.WebRtcCallService; 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.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.ExpirationUtil; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.SettableFuture; import org.thoughtcrime.securesms.util.views.Stub; import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.util.guava.Optional; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ExecutionException; import static org.thoughtcrime.securesms.TransportOption.Type; import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK; import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; /** * Activity for displaying a message thread, as well as * composing/sending a new message into that thread. * * @author Moxie Marlinspike * */ @SuppressLint("StaticFieldLeak") public class ConversationActivity extends PassphraseRequiredActionBarActivity implements ConversationFragment.ConversationFragmentListener, AttachmentManager.AttachmentListener, RecipientModifiedListener, OnKeyboardShownListener, AttachmentDrawerListener, InputPanel.Listener, InputPanel.MediaListener { private static final String TAG = ConversationActivity.class.getSimpleName(); public static final String ADDRESS_EXTRA = "address"; public static final String THREAD_ID_EXTRA = "thread_id"; public static final String IS_ARCHIVED_EXTRA = "is_archived"; public static final String TEXT_EXTRA = "draft_text"; public static final String DISTRIBUTION_TYPE_EXTRA = "distribution_type"; public static final String TIMING_EXTRA = "timing"; public static final String LAST_SEEN_EXTRA = "last_seen"; private static final int PICK_GALLERY = 1; private static final int PICK_DOCUMENT = 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 static final int ADD_CONTACT = 7; private static final int PICK_LOCATION = 8; private static final int PICK_GIF = 9; private static final int SMS_DEFAULT = 10; private MasterSecret masterSecret; private GlideRequests glideRequests; 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 Button makeDefaultSmsButton; private InputAwareLayout container; private View composePanel; protected Stub reminderView; private Stub unverifiedBannerView; private Stub groupShareProfileView; private AttachmentTypeSelector attachmentTypeSelector; private AttachmentManager attachmentManager; private AudioRecorder audioRecorder; private BroadcastReceiver securityUpdateReceiver; private BroadcastReceiver recipientsStaleReceiver; private Stub emojiDrawerStub; protected HidingLinearLayout quickAttachmentToggle; private QuickAttachmentDrawer quickAttachmentDrawer; private InputPanel inputPanel; private Recipient recipient; private long threadId; private int distributionType; private boolean archived; private boolean isSecureText; private boolean isDefaultSms = true; private boolean isMmsEnabled = true; private boolean isSecurityInitialized = false; private final IdentityRecordList identityRecords = new IdentityRecordList(); private final DynamicTheme dynamicTheme = new DynamicTheme(); private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); @Override protected void onPreCreate() { dynamicTheme.onCreate(this); dynamicLanguage.onCreate(this); } @Override protected void onCreate(Bundle state, @NonNull MasterSecret masterSecret) { Log.w(TAG, "onCreate()"); this.masterSecret = masterSecret; supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY); setContentView(R.layout.conversation_activity); TypedArray typedArray = obtainStyledAttributes(new int[] {R.attr.conversation_background}); int color = typedArray.getColor(0, Color.WHITE); typedArray.recycle(); getWindow().getDecorView().setBackgroundColor(color); fragment = initFragment(R.id.fragment_content, new ConversationFragment(), masterSecret, dynamicLanguage.getCurrentLocale()); initializeReceivers(); initializeActionBar(); initializeViews(); initializeResources(); initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean result) { initializeProfiles(); initializeDraft(); } }); } @Override protected void onNewIntent(Intent intent) { Log.w(TAG, "onNewIntent()"); if (isFinishing()) { Log.w(TAG, "Activity is finishing..."); return; } if (!Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent()) { saveDraft(); attachmentManager.clear(glideRequests, false); composeText.setText(""); } setIntent(intent); initializeResources(); initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean result) { initializeDraft(); } }); if (fragment != null) { fragment.onNewIntent(); } } @Override protected void onStart() { super.onStart(); EventBus.getDefault().register(this); } @Override protected void onResume() { super.onResume(); dynamicTheme.onResume(this); dynamicLanguage.onResume(this); quickAttachmentDrawer.onResume(); initializeEnabledCheck(); initializeMmsEnabledCheck(); initializeIdentityRecords(); composeText.setTransport(sendButton.getSelectedTransport()); titleView.setTitle(glideRequests, recipient); setActionBarColor(recipient.getColor()); setBlockedUserState(recipient, isSecureText, isDefaultSms); setGroupShareProfileReminder(recipient); calculateCharactersRemaining(); MessageNotifier.setVisibleThread(threadId); markThreadAsRead(); Log.w(TAG, "onResume() Finished: " + (System.currentTimeMillis() - getIntent().getLongExtra(TIMING_EXTRA, 0))); } @Override protected void onPause() { super.onPause(); MessageNotifier.setVisibleThread(-1L); if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right); quickAttachmentDrawer.onPause(); inputPanel.onPause(); fragment.setLastSeen(System.currentTimeMillis()); markLastSeen(); AudioSlidePlayer.stopAll(); EventBus.getDefault().unregister(this); } @Override protected void onStop() { super.onStop(); EventBus.getDefault().unregister(this); } @Override public void onConfigurationChanged(Configuration newConfig) { Log.w(TAG, "onConfigurationChanged(" + newConfig.orientation + ")"); super.onConfigurationChanged(newConfig); composeText.setTransport(sendButton.getSelectedTransport()); quickAttachmentDrawer.onConfigurationChanged(); if (emojiDrawerStub.resolved() && container.getCurrentInput() == emojiDrawerStub.get()) { container.hideAttachedInput(true); } } @Override protected void onDestroy() { saveDraft(); if (recipient != null) recipient.removeListener(this); if (securityUpdateReceiver != null) unregisterReceiver(securityUpdateReceiver); if (recipientsStaleReceiver != null) unregisterReceiver(recipientsStaleReceiver); super.onDestroy(); } @Override public void onActivityResult(final 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 && reqCode != SMS_DEFAULT) || (resultCode != RESULT_OK && reqCode != SMS_DEFAULT)) { return; } switch (reqCode) { case PICK_GALLERY: MediaType mediaType; String mimeType = MediaUtil.getMimeType(this, data.getData()); if (MediaUtil.isGif(mimeType)) mediaType = MediaType.GIF; else if (MediaUtil.isVideo(mimeType)) mediaType = MediaType.VIDEO; else mediaType = MediaType.IMAGE; setMedia(data.getData(), mediaType); break; case PICK_DOCUMENT: setMedia(data.getData(), MediaType.DOCUMENT); break; case PICK_AUDIO: setMedia(data.getData(), MediaType.AUDIO); break; case PICK_CONTACT_INFO: addAttachmentContactInfo(data.getData()); break; case GROUP_EDIT: recipient = Recipient.from(this, data.getParcelableExtra(GroupCreateActivity.GROUP_ADDRESS_EXTRA), true); recipient.addListener(this); titleView.setTitle(glideRequests, recipient); setBlockedUserState(recipient, isSecureText, isDefaultSms); supportInvalidateOptionsMenu(); break; case TAKE_PHOTO: if (attachmentManager.getCaptureUri() != null) { setMedia(attachmentManager.getCaptureUri(), MediaType.IMAGE); } break; case ADD_CONTACT: recipient = Recipient.from(this, recipient.getAddress(), true); recipient.addListener(this); fragment.reloadList(); break; case PICK_LOCATION: SignalPlace place = new SignalPlace(PlacePicker.getPlace(data, this)); attachmentManager.setLocation(masterSecret, place, getCurrentMediaConstraints()); break; case PICK_GIF: setMedia(data.getData(), MediaType.GIF); break; case ScribbleActivity.SCRIBBLE_REQUEST_CODE: setMedia(data.getData(), MediaType.IMAGE); break; case SMS_DEFAULT: initializeSecurity(isSecureText, isDefaultSms); break; } } @Override public void startActivity(Intent intent) { if (intent.getStringExtra(Browser.EXTRA_APPLICATION_ID) != null) { intent.removeExtra(Browser.EXTRA_APPLICATION_ID); } try { super.startActivity(intent); } catch (ActivityNotFoundException e) { Log.w(TAG, e); Toast.makeText(this, R.string.ConversationActivity_there_is_no_app_available_to_handle_this_link_on_your_device, Toast.LENGTH_LONG).show(); } } @Override public boolean onPrepareOptionsMenu(Menu menu) { MenuInflater inflater = this.getMenuInflater(); menu.clear(); if (isSecureText) { if (recipient.getExpireMessages() > 0) { inflater.inflate(R.menu.conversation_expiring_on, menu); final MenuItem item = menu.findItem(R.id.menu_expiring_messages); final View actionView = MenuItemCompat.getActionView(item); final TextView badgeView = (TextView)actionView.findViewById(R.id.expiration_badge); badgeView.setText(ExpirationUtil.getExpirationAbbreviatedDisplayValue(this, recipient.getExpireMessages())); actionView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { onOptionsItemSelected(item); } }); } else { inflater.inflate(R.menu.conversation_expiring_off, menu); } } if (isSingleConversation()) { if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu); else inflater.inflate(R.menu.conversation_callable_insecure, 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 (isSingleConversation() && isSecureText) { inflater.inflate(R.menu.conversation_secure, menu); } else if (isSingleConversation()) { inflater.inflate(R.menu.conversation_insecure, menu); } if (recipient != null && recipient.isMuted()) inflater.inflate(R.menu.conversation_muted, menu); else inflater.inflate(R.menu.conversation_unmuted, menu); if (isSingleConversation() && getRecipient().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_secure: case R.id.menu_call_insecure: handleDial(getRecipient()); 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_reset_secure_session: handleResetSecureSession(); 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 R.id.menu_expiring_messages_off: case R.id.menu_expiring_messages: handleSelectMessageExpiration(); return true; case android.R.id.home: handleReturnToConversationList(); return true; } return false; } @Override public void onBackPressed() { Log.w(TAG, "onBackPressed()"); if (container.isInputOpen()) container.hideCurrentInput(composeText); else super.onBackPressed(); } @Override public void onKeyboardShown() { inputPanel.onKeyboardShown(); } @Subscribe(threadMode = ThreadMode.MAIN) public void onEvent(ReminderUpdateEvent event) { updateReminders(recipient.hasSeenInviteReminder()); } //////// Event Handlers private void handleReturnToConversationList() { Intent intent = new Intent(this, (archived ? ConversationListArchiveActivity.class : ConversationListActivity.class)); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); finish(); } private void handleSelectMessageExpiration() { if (isPushGroupConversation() && !isActiveGroup()) { return; } ExpirationDialog.show(this, recipient.getExpireMessages(), new ExpirationDialog.OnClickListener() { @Override public void onClick(final int expirationTime) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient, expirationTime); OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(getRecipient(), System.currentTimeMillis(), expirationTime * 1000); MessageSender.send(ConversationActivity.this, masterSecret, outgoingMessage, threadId, false, null); return null; } @Override protected void onPostExecute(Void result) { invalidateOptionsMenu(); if (fragment != null) fragment.setLastSeen(0); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } }); } private void handleMuteNotifications() { MuteDialog.show(this, new MuteDialog.MuteSelectionListener() { @Override public void onMuted(final long until) { recipient.setMuted(until); new AsyncTask() { @Override protected Void doInBackground(Void... params) { DatabaseFactory.getRecipientDatabase(ConversationActivity.this) .setMuted(recipient, until); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } }); } private void handleConversationSettings() { Intent intent = new Intent(ConversationActivity.this, RecipientPreferenceActivity.class); intent.putExtra(RecipientPreferenceActivity.ADDRESS_EXTRA, recipient.getAddress()); intent.putExtra(RecipientPreferenceActivity.CAN_HAVE_SAFETY_NUMBER_EXTRA, isSecureText && !isSelfConversation()); startActivitySceneTransition(intent, titleView.findViewById(R.id.contact_photo_image), "avatar"); } private void handleUnmuteNotifications() { recipient.setMuted(0); new AsyncTask() { @Override protected Void doInBackground(Void... params) { DatabaseFactory.getRecipientDatabase(ConversationActivity.this) .setMuted(recipient, 0); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private void handleUnblock() { new AlertDialog.Builder(this) .setTitle(R.string.ConversationActivity_unblock_this_contact_question) .setMessage(R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.ConversationActivity_unblock, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { DatabaseFactory.getRecipientDatabase(ConversationActivity.this) .setBlocked(recipient, false); ApplicationContext.getInstance(ConversationActivity.this) .getJobManager() .add(new MultiDeviceBlockedUpdateJob(ConversationActivity.this)); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } }).show(); } @TargetApi(Build.VERSION_CODES.KITKAT) private void handleMakeDefaultSms() { Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT); intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, getPackageName()); startActivityForResult(intent, SMS_DEFAULT); } private void handleInviteLink() { try { String inviteText; boolean a = SecureRandom.getInstance("SHA1PRNG").nextBoolean(); if (a) inviteText = getString(R.string.ConversationActivity_lets_switch_to_signal, "https://sgnl.link/1LoIMUl"); else inviteText = getString(R.string.ConversationActivity_lets_use_this_to_chat, "https://sgnl.link/1MF56H1"); if (isDefaultSms) { composeText.appendInvite(inviteText); } else { Intent intent = new Intent(Intent.ACTION_SENDTO); intent.setData(Uri.parse("smsto:" + recipient.getAddress().serialize())); intent.putExtra("sms_body", inviteText); intent.putExtra(Intent.EXTRA_TEXT, inviteText); startActivity(intent); } } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } } private void handleResetSecureSession() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.ConversationActivity_reset_secure_session_question); builder.setIconAttribute(R.attr.dialog_alert_icon); builder.setCancelable(true); builder.setMessage(R.string.ConversationActivity_this_may_help_if_youre_having_encryption_problems); builder.setPositiveButton(R.string.ConversationActivity_reset, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (isSingleConversation()) { final Context context = getApplicationContext(); OutgoingEndSessionMessage endSessionMessage = new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipient(), "TERMINATE", 0, -1)); new AsyncTask() { @Override protected Long doInBackground(OutgoingEndSessionMessage... messages) { return MessageSender.send(context, masterSecret, messages[0], threadId, false, null); } @Override protected void onPostExecute(Long result) { sendComplete(result); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, endSessionMessage); } } }); builder.setNegativeButton(android.R.string.cancel, null); builder.show(); } private void handleViewMedia() { Intent intent = new Intent(this, MediaOverviewActivity.class); intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, recipient.getAddress()); startActivity(intent); } private void handleLeavePushGroup() { if (getRecipient() == null) { Toast.makeText(this, getString(R.string.ConversationActivity_invalid_recipient), Toast.LENGTH_LONG).show(); return; } AlertDialog.Builder builder = new AlertDialog.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 { String groupId = getRecipient().getAddress().toGroupString(); DatabaseFactory.getGroupDatabase(self).setActive(groupId, false); GroupContext context = GroupContext.newBuilder() .setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupId))) .setType(GroupContext.Type.QUIT) .build(); OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(getRecipient(), context, null, System.currentTimeMillis(), 0); MessageSender.send(self, masterSecret, outgoingMessage, threadId, false, null); DatabaseFactory.getGroupDatabase(self).remove(groupId, Address.fromSerialized(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_ADDRESS_EXTRA, recipient.getAddress()); startActivityForResult(intent, GROUP_EDIT); } private void handleDistributionBroadcastEnabled(MenuItem item) { distributionType = ThreadDatabase.DistributionTypes.BROADCAST; item.setChecked(true); if (threadId != -1) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { DatabaseFactory.getThreadDatabase(ConversationActivity.this) .setDistributionType(threadId, ThreadDatabase.DistributionTypes.BROADCAST); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } private void handleDistributionConversationEnabled(MenuItem item) { distributionType = ThreadDatabase.DistributionTypes.CONVERSATION; item.setChecked(true); if (threadId != -1) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { DatabaseFactory.getThreadDatabase(ConversationActivity.this) .setDistributionType(threadId, ThreadDatabase.DistributionTypes.CONVERSATION); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } private void handleDial(final Recipient recipient) { if (recipient == null) return; if (isSecureText) { Intent intent = new Intent(this, WebRtcCallService.class); intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL); intent.putExtra(WebRtcCallService.EXTRA_REMOTE_ADDRESS, recipient.getAddress()); startService(intent); Intent activityIntent = new Intent(this, WebRtcCallActivity.class); activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(activityIntent); } else { try { Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + recipient.getAddress().serialize())); 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, getRecipient()).display(); } private void handleAddToContacts() { if (recipient.getAddress().isGroup()) return; try { final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); if (recipient.getAddress().isEmail()) { intent.putExtra(ContactsContract.Intents.Insert.EMAIL, recipient.getAddress().toEmailString()); } else { intent.putExtra(ContactsContract.Intents.Insert.PHONE, recipient.getAddress().toPhoneString()); } intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); startActivityForResult(intent, ADD_CONTACT); } catch (ActivityNotFoundException e) { Log.w(TAG, e); } } private boolean handleDisplayQuickContact() { if (recipient.getAddress().isGroup()) return false; if (recipient.getContactUri() != null) { ContactsContract.QuickContact.showQuickContact(ConversationActivity.this, titleView, recipient.getContactUri(), ContactsContract.QuickContact.MODE_LARGE, null); } else { handleAddToContacts(); } return true; } private void handleAddAttachment() { if (this.isMmsEnabled || isSecureText) { if (attachmentTypeSelector == null) { attachmentTypeSelector = new AttachmentTypeSelector(this, getSupportLoaderManager(), new AttachmentTypeListener()); } attachmentTypeSelector.show(this, attachButton); } 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); } private void handleUnverifiedRecipients() { List unverifiedRecipients = identityRecords.getUnverifiedRecipients(this); List unverifiedRecords = identityRecords.getUnverifiedRecords(); String message = IdentityUtil.getUnverifiedSendDialogDescription(this, unverifiedRecipients); if (message == null) return; new UnverifiedSendDialog(this, message, unverifiedRecords, new UnverifiedSendDialog.ResendListener() { @Override public void onResendMessage() { initializeIdentityRecords().addListener(new ListenableFuture.Listener() { @Override public void onSuccess(Boolean result) { sendMessage(); } @Override public void onFailure(ExecutionException e) { throw new AssertionError(e); } }); } }).show(); } private void handleUntrustedRecipients() { List untrustedRecipients = identityRecords.getUntrustedRecipients(this); List untrustedRecords = identityRecords.getUntrustedRecords(); String untrustedMessage = IdentityUtil.getUntrustedSendDialogDescription(this, untrustedRecipients); if (untrustedMessage == null) return; new UntrustedSendDialog(this, untrustedMessage, untrustedRecords, new UntrustedSendDialog.ResendListener() { @Override public void onResendMessage() { initializeIdentityRecords().addListener(new ListenableFuture.Listener() { @Override public void onSuccess(Boolean result) { sendMessage(); } @Override public void onFailure(ExecutionException e) { throw new AssertionError(e); } }); } }).show(); } private void handleSecurityChange(boolean isSecureText, boolean isDefaultSms) { Log.w(TAG, "handleSecurityChange(" + isSecureText + ", " + isDefaultSms + ")"); if (isSecurityInitialized && isSecureText == this.isSecureText && isDefaultSms == this.isDefaultSms) { return; } this.isSecureText = isSecureText; this.isDefaultSms = isDefaultSms; this.isSecurityInitialized = true; boolean isMediaMessage = recipient.isMmsGroupRecipient() || attachmentManager.isAttachmentPresent(); sendButton.resetAvailableTransports(isMediaMessage); if (!isSecureText) sendButton.disableTransport(Type.TEXTSECURE); if (recipient.isPushGroupRecipient()) sendButton.disableTransport(Type.SMS); if (isSecureText) sendButton.setDefaultTransport(Type.TEXTSECURE); else sendButton.setDefaultTransport(Type.SMS); calculateCharactersRemaining(); supportInvalidateOptionsMenu(); setBlockedUserState(recipient, isSecureText, isDefaultSms); } ///// Initializers private void initializeDraft() { final String draftText = getIntent().getStringExtra(TEXT_EXTRA); final Uri draftMedia = getIntent().getData(); final MediaType draftMediaType = MediaType.from(getIntent().getType()); if (draftText != null) composeText.setText(draftText); if (draftMedia != null && draftMediaType != null) setMedia(draftMedia, draftMediaType); if (draftText == null && draftMedia == null && draftMediaType == null) { initializeDraftFromDatabase(); } else { updateToggleButtonState(); } } private void initializeEnabledCheck() { boolean enabled = !(isPushGroupConversation() && !isActiveGroup()); inputPanel.setEnabled(enabled); sendButton.setEnabled(enabled); attachButton.setEnabled(enabled); } private void initializeDraftFromDatabase() { new AsyncTask>() { @Override protected List doInBackground(Void... params) { MasterCipher masterCipher = new MasterCipher(masterSecret); DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this); List results = draftDatabase.getDrafts(masterCipher, threadId); draftDatabase.clearDrafts(threadId); return results; } @Override protected void onPostExecute(List drafts) { for (Draft draft : drafts) { try { if (draft.getType().equals(Draft.TEXT)) { composeText.setText(draft.getValue()); } else if (draft.getType().equals(Draft.LOCATION)) { attachmentManager.setLocation(masterSecret, SignalPlace.deserialize(draft.getValue()), getCurrentMediaConstraints()); } else if (draft.getType().equals(Draft.IMAGE)) { setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE); } else if (draft.getType().equals(Draft.AUDIO)) { setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO); } else if (draft.getType().equals(Draft.VIDEO)) { setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO); } } catch (IOException e) { Log.w(TAG, e); } } updateToggleButtonState(); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private ListenableFuture initializeSecurity(final boolean currentSecureText, final boolean currentIsDefaultSms) { final SettableFuture future = new SettableFuture<>(); handleSecurityChange(currentSecureText || isPushGroupConversation(), currentIsDefaultSms); new AsyncTask() { @Override protected boolean[] doInBackground(Recipient... params) { Context context = ConversationActivity.this; Recipient recipient = params[0]; Log.w(TAG, "Resolving registered state..."); RegisteredState registeredState = recipient.resolve().getRegistered(); Log.w(TAG, "Resolved registered state: " + registeredState); boolean signalEnabled = TextSecurePreferences.isPushRegistered(context); if (registeredState == RegisteredState.UNKNOWN) { try { Log.w(TAG, "Refreshing directory for user: " + recipient.getAddress().serialize()); registeredState = DirectoryHelper.refreshDirectoryFor(context, masterSecret, recipient); } catch (IOException e) { Log.w(TAG, e); } } Log.w(TAG, "Returning registered state..."); return new boolean[] {registeredState == RegisteredState.REGISTERED && signalEnabled, Util.isDefaultSmsProvider(context)}; } @Override protected void onPostExecute(boolean[] result) { if (result[0] != currentSecureText || result[1] != currentIsDefaultSms) { Log.w(TAG, "onPostExecute() handleSecurityChange: " + result[0] + " , " + result[1]); handleSecurityChange(result[0], result[1]); } future.set(true); onSecurityUpdated(); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, recipient); return future; } private void onSecurityUpdated() { Log.w(TAG, "onSecurityUpdated()"); updateReminders(recipient.hasSeenInviteReminder()); updateDefaultSubscriptionId(recipient.getDefaultSubscriptionId()); } protected void updateReminders(boolean seenInvite) { Log.w(TAG, "updateReminders(" + seenInvite+")"); if (UnauthorizedReminder.isEligible(this)) { reminderView.get().showReminder(new UnauthorizedReminder(this)); } else if (ExpiredBuildReminder.isEligible()) { reminderView.get().showReminder(new ExpiredBuildReminder(this)); } else if (TextSecurePreferences.isPushRegistered(this) && TextSecurePreferences.isShowInviteReminders(this) && !isSecureText && !seenInvite && !recipient.isGroupRecipient()) { InviteReminder reminder = new InviteReminder(this, recipient); reminder.setOkListener(v -> { handleInviteLink(); reminderView.get().requestDismiss(); }); reminderView.get().showReminder(reminder); } else if (reminderView.resolved()) { reminderView.get().hide(); } } private void updateDefaultSubscriptionId(Optional defaultSubscriptionId) { Log.w(TAG, "updateDefaultSubscriptionId(" + defaultSubscriptionId.orNull() + ")"); sendButton.setDefaultSubscriptionId(defaultSubscriptionId); } private void initializeMmsEnabledCheck() { new AsyncTask() { @Override protected Boolean doInBackground(Void... params) { return Util.isMmsCapable(ConversationActivity.this); } @Override protected void onPostExecute(Boolean isMmsEnabled) { ConversationActivity.this.isMmsEnabled = isMmsEnabled; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private ListenableFuture initializeIdentityRecords() { final SettableFuture future = new SettableFuture<>(); new AsyncTask>() { @Override protected @NonNull Pair doInBackground(Recipient... params) { IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ConversationActivity.this); IdentityRecordList identityRecordList = new IdentityRecordList(); List recipients = new LinkedList<>(); if (params[0].isGroupRecipient()) { recipients.addAll(DatabaseFactory.getGroupDatabase(ConversationActivity.this) .getGroupMembers(params[0].getAddress().toGroupString(), false)); } else { recipients.add(params[0]); } for (Recipient recipient : recipients) { Log.w(TAG, "Loading identity for: " + recipient.getAddress()); identityRecordList.add(identityDatabase.getIdentity(recipient.getAddress())); } String message = null; if (identityRecordList.isUnverified()) { message = IdentityUtil.getUnverifiedBannerDescription(ConversationActivity.this, identityRecordList.getUnverifiedRecipients(ConversationActivity.this)); } return new Pair<>(identityRecordList, message); } @Override protected void onPostExecute(@NonNull Pair result) { Log.w(TAG, "Got identity records: " + result.first.isUnverified()); identityRecords.replaceWith(result.first); if (result.second != null) { Log.w(TAG, "Replacing banner..."); unverifiedBannerView.get().display(result.second, result.first.getUnverifiedRecords(), new UnverifiedClickedListener(), new UnverifiedDismissedListener()); } else if (unverifiedBannerView.resolved()) { Log.w(TAG, "Clearing banner..."); unverifiedBannerView.get().hide(); } titleView.setVerified(isSecureText && identityRecords.isVerified()); future.set(true); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, recipient); return future; } private void initializeViews() { titleView = (ConversationTitleView) getSupportActionBar().getCustomView(); buttonToggle = ViewUtil.findById(this, R.id.button_toggle); sendButton = ViewUtil.findById(this, R.id.send_button); attachButton = ViewUtil.findById(this, R.id.attach_button); composeText = ViewUtil.findById(this, R.id.embedded_text_editor); charactersLeft = ViewUtil.findById(this, R.id.space_left); emojiDrawerStub = ViewUtil.findStubById(this, R.id.emoji_drawer_stub); unblockButton = ViewUtil.findById(this, R.id.unblock_button); makeDefaultSmsButton = ViewUtil.findById(this, R.id.make_default_sms_button); composePanel = ViewUtil.findById(this, R.id.bottom_panel); container = ViewUtil.findById(this, R.id.layout_container); reminderView = ViewUtil.findStubById(this, R.id.reminder_stub); unverifiedBannerView = ViewUtil.findStubById(this, R.id.unverified_banner_stub); groupShareProfileView = ViewUtil.findStubById(this, R.id.group_share_profile_view_stub); quickAttachmentDrawer = ViewUtil.findById(this, R.id.quick_attachment_drawer); quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle); inputPanel = ViewUtil.findById(this, R.id.bottom_panel); ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle); View composeBubble = ViewUtil.findById(this, R.id.compose_bubble); container.addOnKeyboardShownListener(this); inputPanel.setListener(this); inputPanel.setMediaListener(this); 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(); attachmentTypeSelector = null; attachmentManager = new AttachmentManager(this, this); audioRecorder = new AudioRecorder(this, masterSecret); SendButtonListener sendButtonListener = new SendButtonListener(); ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener(); composeText.setOnEditorActionListener(sendButtonListener); attachButton.setOnClickListener(new AttachButtonListener()); attachButton.setOnLongClickListener(new AttachButtonLongClickListener()); sendButton.setOnClickListener(sendButtonListener); sendButton.setEnabled(true); sendButton.addOnTransportChangedListener(new OnTransportChangedListener() { @Override public void onChange(TransportOption newTransport, boolean manuallySelected) { calculateCharactersRemaining(); composeText.setTransport(newTransport); buttonToggle.getBackground().setColorFilter(newTransport.getBackgroundColor(), Mode.MULTIPLY); buttonToggle.getBackground().invalidateSelf(); if (manuallySelected) recordSubscriptionIdPreference(newTransport.getSimSubscriptionId()); } }); titleView.setOnClickListener(v -> handleConversationSettings()); titleView.setOnLongClickListener(v -> handleDisplayQuickContact()); titleView.setOnBackClickedListener(view -> super.onBackPressed()); unblockButton.setOnClickListener(v -> handleUnblock()); makeDefaultSmsButton.setOnClickListener(v -> handleMakeDefaultSms()); composeText.setOnKeyListener(composeKeyPressedListener); composeText.addTextChangedListener(composeKeyPressedListener); composeText.setOnEditorActionListener(sendButtonListener); composeText.setOnClickListener(composeKeyPressedListener); composeText.setOnFocusChangeListener(composeKeyPressedListener); if (QuickAttachmentDrawer.isDeviceSupported(this)) { quickAttachmentDrawer.setListener(this); quickCameraToggle.setOnClickListener(new QuickCameraToggleListener()); } else { quickCameraToggle.setVisibility(View.GONE); quickCameraToggle.setEnabled(false); } } protected void initializeActionBar() { getSupportActionBar().setDisplayHomeAsUpEnabled(false); getSupportActionBar().setCustomView(R.layout.conversation_title_view); getSupportActionBar().setDisplayShowCustomEnabled(true); getSupportActionBar().setDisplayShowTitleEnabled(false); } private void initializeResources() { if (recipient != null) recipient.removeListener(this); recipient = Recipient.from(this, (Address)getIntent().getParcelableExtra(ADDRESS_EXTRA), true); threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1); archived = getIntent().getBooleanExtra(IS_ARCHIVED_EXTRA, false); distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT); glideRequests = GlideApp.with(this); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { LinearLayout conversationContainer = ViewUtil.findById(this, R.id.conversation_container); conversationContainer.setClipChildren(true); conversationContainer.setClipToPadding(true); } recipient.addListener(this); } private void initializeProfiles() { if (!isSecureText) { Log.w(TAG, "SMS contact, no profile fetch needed."); return; } ApplicationContext.getInstance(this) .getJobManager() .add(new RetrieveProfileJob(this, recipient)); } @Override public void onModified(final Recipient recipient) { Log.w(TAG, "onModified(" + recipient.getAddress().serialize() + ")"); Util.runOnMain(() -> { Log.w(TAG, "onModifiedRun(): " + recipient.getRegistered()); titleView.setTitle(glideRequests, recipient); titleView.setVerified(identityRecords.isVerified()); setBlockedUserState(recipient, isSecureText, isDefaultSms); setActionBarColor(recipient.getColor()); setGroupShareProfileReminder(recipient); updateReminders(recipient.hasSeenInviteReminder()); updateDefaultSubscriptionId(recipient.getDefaultSubscriptionId()); initializeSecurity(isSecureText, isDefaultSms); invalidateOptionsMenu(); }); } @Subscribe(threadMode = ThreadMode.MAIN) public void onIdentityRecordUpdate(final IdentityRecord event) { initializeIdentityRecords(); } private void initializeReceivers() { securityUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { initializeSecurity(isSecureText, isDefaultSms); calculateCharactersRemaining(); } }; recipientsStaleReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.w(TAG, "Group update received..."); if (recipient != null) { Log.w(TAG, "Looking up new recipients..."); recipient = Recipient.from(context, recipient.getAddress(), true); recipient.addListener(ConversationActivity.this); onModified(recipient); fragment.reloadList(); } } }; IntentFilter staleFilter = new IntentFilter(); staleFilter.addAction(GroupDatabase.DATABASE_UPDATE_ACTION); staleFilter.addAction(Recipient.RECIPIENT_CLEAR_ACTION); registerReceiver(securityUpdateReceiver, new IntentFilter(SecurityEvent.SECURITY_UPDATE_EVENT), KeyCachingService.KEY_PERMISSION, null); registerReceiver(recipientsStaleReceiver, staleFilter); } //////// Helper Methods private void addAttachment(int type) { Log.w("ComposeMessageActivity", "Selected: " + type); switch (type) { case AttachmentTypeSelector.ADD_GALLERY: AttachmentManager.selectGallery(this, PICK_GALLERY); break; case AttachmentTypeSelector.ADD_DOCUMENT: AttachmentManager.selectDocument(this, PICK_DOCUMENT); break; case AttachmentTypeSelector.ADD_SOUND: AttachmentManager.selectAudio(this, PICK_AUDIO); break; case AttachmentTypeSelector.ADD_CONTACT_INFO: AttachmentManager.selectContactInfo(this, PICK_CONTACT_INFO); break; case AttachmentTypeSelector.ADD_LOCATION: AttachmentManager.selectLocation(this, PICK_LOCATION); break; case AttachmentTypeSelector.TAKE_PHOTO: attachmentManager.capturePhoto(this, TAKE_PHOTO); break; case AttachmentTypeSelector.ADD_GIF: AttachmentManager.selectGif(this, PICK_GIF, !isSecureText); break; } } private void setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) { if (uri == null) return; attachmentManager.setMedia(masterSecret, glideRequests, uri, mediaType, getCurrentMediaConstraints()); } 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; } AlertDialog.Builder builder = new AlertDialog.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.getTextTrimmed())); } for (Slide slide : attachmentManager.buildSlideDeck().getSlides()) { if (slide.hasAudio() && slide.getUri() != null) drafts.add(new Draft(Draft.AUDIO, slide.getUri().toString())); else if (slide.hasVideo() && slide.getUri() != null) drafts.add(new Draft(Draft.VIDEO, slide.getUri().toString())); else if (slide.hasLocation()) drafts.add(new Draft(Draft.LOCATION, ((LocationSlide)slide).getPlace().serialize())); else if (slide.hasImage() && slide.getUri() != null) drafts.add(new Draft(Draft.IMAGE, slide.getUri().toString())); } return drafts; } protected ListenableFuture saveDraft() { final SettableFuture future = new SettableFuture<>(); if (this.recipient == null) { 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() { @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(getRecipient(), thisDistributionType); draftDatabase.insertDrafts(new MasterCipher(thisMasterSecret), threadId, drafts); threadDatabase.updateSnippet(threadId, drafts.getSnippet(ConversationActivity.this), drafts.getUriSnippet(ConversationActivity.this), System.currentTimeMillis(), Types.BASE_DRAFT_TYPE, true); } else if (threadId > 0) { threadDatabase.update(threadId, false); } return threadId; } @Override protected void onPostExecute(Long result) { future.set(result); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, thisThreadId); return future; } private void setActionBarColor(MaterialColor color) { getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this))); setStatusBarColor(color.toStatusBarColor(this)); } private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) { if (recipient.isBlocked()) { unblockButton.setVisibility(View.VISIBLE); composePanel.setVisibility(View.GONE); makeDefaultSmsButton.setVisibility(View.GONE); } else if (!isSecureText && !isDefaultSms) { unblockButton.setVisibility(View.GONE); composePanel.setVisibility(View.GONE); makeDefaultSmsButton.setVisibility(View.VISIBLE); } else { composePanel.setVisibility(View.VISIBLE); unblockButton.setVisibility(View.GONE); makeDefaultSmsButton.setVisibility(View.GONE); } } private void setGroupShareProfileReminder(@NonNull Recipient recipient) { if (recipient.isPushGroupRecipient() && !recipient.isProfileSharing()) { groupShareProfileView.get().setRecipient(recipient); groupShareProfileView.get().setVisibility(View.VISIBLE); } else if (groupShareProfileView.resolved()) { groupShareProfileView.get().setVisibility(View.GONE); } } private void calculateCharactersRemaining() { String messageBody = composeText.getTextTrimmed(); TransportOption transportOption = sendButton.getSelectedTransport(); CharacterState characterState = transportOption.calculateCharacters(messageBody); 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 getRecipient() != null && !getRecipient().isGroupRecipient(); } private boolean isActiveGroup() { if (!isGroupConversation()) return false; Optional record = DatabaseFactory.getGroupDatabase(this).getGroup(getRecipient().getAddress().toGroupString()); return record.isPresent() && record.get().isActive(); } private boolean isSelfConversation() { if (!TextSecurePreferences.isPushRegistered(this)) return false; if (recipient.isGroupRecipient()) return false; return Util.isOwnNumber(this, recipient.getAddress()); } private boolean isGroupConversation() { return getRecipient() != null && getRecipient().isGroupRecipient(); } private boolean isPushGroupConversation() { return getRecipient() != null && getRecipient().isPushGroupRecipient(); } protected Recipient getRecipient() { return this.recipient; } protected long getThreadId() { return this.threadId; } private String getMessage() throws InvalidMessageException { String rawText = composeText.getTextTrimmed(); if (rawText.length() < 1 && !attachmentManager.isAttachmentPresent()) throw new InvalidMessageException(getString(R.string.ConversationActivity_message_is_empty_exclamation)); return rawText; } private MediaConstraints getCurrentMediaConstraints() { return sendButton.getSelectedTransport().getType() == Type.TEXTSECURE ? MediaConstraints.getPushMediaConstraints() : MediaConstraints.getMmsMediaConstraints(sendButton.getSelectedTransport().getSimSubscriptionId().or(-1)); } private void markThreadAsRead() { new AsyncTask() { @Override protected Void doInBackground(Long... params) { Context context = ConversationActivity.this; List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(params[0], false); MessageNotifier.updateNotification(context, masterSecret); MarkReadReceiver.process(context, messageIds); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId); } private void markLastSeen() { new AsyncTask() { @Override protected Void doInBackground(Long... params) { DatabaseFactory.getThreadDatabase(ConversationActivity.this).setLastSeen(params[0]); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId); } protected void sendComplete(long threadId) { boolean refreshFragment = (threadId != this.threadId); this.threadId = threadId; if (fragment == null || !fragment.isVisible() || isFinishing()) { return; } fragment.setLastSeen(0); if (refreshFragment) { fragment.reload(recipient, threadId); MessageNotifier.setVisibleThread(threadId); } fragment.scrollToBottom(); attachmentManager.cleanup(); } private void sendMessage() { try { Recipient recipient = getRecipient(); if (recipient == null) { throw new RecipientFormattingException("Badly formatted"); } boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms(); int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1); long expiresIn = recipient.getExpireMessages() * 1000; boolean initiating = threadId == -1; Log.w(TAG, "isManual Selection: " + sendButton.isManualSelection()); Log.w(TAG, "forceSms: " + forceSms); if ((recipient.isMmsGroupRecipient() || recipient.getAddress().isEmail()) && !isMmsEnabled) { handleManualMmsRequired(); } else if (!forceSms && identityRecords.isUnverified()) { handleUnverifiedRecipients(); } else if (!forceSms && identityRecords.isUntrusted()) { handleUntrustedRecipients(); } else if (attachmentManager.isAttachmentPresent() || recipient.isGroupRecipient() || recipient.getAddress().isEmail()) { sendMediaMessage(forceSms, expiresIn, subscriptionId, initiating); } else { sendTextMessage(forceSms, expiresIn, subscriptionId, initiating); } } 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, final long expiresIn, final int subscriptionId, boolean initiating) throws InvalidMessageException { sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), expiresIn, subscriptionId, initiating); } private ListenableFuture sendMediaMessage(final boolean forceSms, String body, SlideDeck slideDeck, final long expiresIn, final int subscriptionId, final boolean initiating) throws InvalidMessageException { final SettableFuture future = new SettableFuture<>(); final Context context = getApplicationContext(); OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType); if (isSecureText && !forceSms) { outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessage); } attachmentManager.clear(glideRequests, false); composeText.setText(""); final long id = fragment.stageOutgoingMessage(outgoingMessage); new AsyncTask() { @Override protected Long doInBackground(OutgoingMediaMessage... messages) { if (initiating) { DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); } return MessageSender.send(context, masterSecret, messages[0], threadId, forceSms, new SmsDatabase.InsertListener() { @Override public void onComplete() { fragment.releaseOutgoingMessage(id); } }); } @Override protected void onPostExecute(Long result) { sendComplete(result); future.set(null); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, outgoingMessage); return future; } private void sendTextMessage(final boolean forceSms, final long expiresIn, final int subscriptionId, final boolean initiatingConversation) throws InvalidMessageException { final Context context = getApplicationContext(); OutgoingTextMessage message; if (isSecureText && !forceSms) { message = new OutgoingEncryptedMessage(recipient, getMessage(), expiresIn); } else { message = new OutgoingTextMessage(recipient, getMessage(), expiresIn, subscriptionId); } this.composeText.setText(""); final long id = fragment.stageOutgoingMessage(message); new AsyncTask() { @Override protected Long doInBackground(OutgoingTextMessage... messages) { if (initiatingConversation) { DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); } return MessageSender.send(context, masterSecret, messages[0], threadId, forceSms, new SmsDatabase.InsertListener() { @Override public void onComplete() { fragment.releaseOutgoingMessage(id); } }); } @Override protected void onPostExecute(Long result) { sendComplete(result); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message); } private void updateToggleButtonState() { if (composeText.getTextTrimmed().length() == 0 && !attachmentManager.isAttachmentPresent()) { buttonToggle.display(attachButton); quickAttachmentToggle.show(); } else { buttonToggle.display(sendButton); quickAttachmentToggle.hide(); } } private void recordSubscriptionIdPreference(final Optional subscriptionId) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { DatabaseFactory.getRecipientDatabase(ConversationActivity.this) .setDefaultSubscriptionId(recipient, subscriptionId.or(-1)); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @Override public void onAttachmentDrawerStateChanged(DrawerState drawerState) { if (drawerState == DrawerState.FULL_EXPANDED) { getSupportActionBar().hide(); } else { getSupportActionBar().show(); } if (drawerState == DrawerState.COLLAPSED) { container.hideAttachedInput(true); } } @Override public void onImageCapture(@NonNull final byte[] imageBytes) { setMedia(PersistentBlobProvider.getInstance(this) .create(masterSecret, imageBytes, MediaUtil.IMAGE_JPEG, null), MediaType.IMAGE); quickAttachmentDrawer.hide(false); } @Override public void onCameraFail() { Toast.makeText(this, R.string.ConversationActivity_quick_camera_unavailable, Toast.LENGTH_SHORT).show(); quickAttachmentDrawer.hide(false); quickAttachmentToggle.disable(); } @Override public void onCameraStart() {} @Override public void onCameraStop() {} @Override public void onRecorderStarted() { Vibrator vibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE); vibrator.vibrate(20); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); audioRecorder.startRecording(); } @Override public void onRecorderFinished() { Vibrator vibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE); vibrator.vibrate(20); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); ListenableFuture> future = audioRecorder.stopRecording(); future.addListener(new ListenableFuture.Listener>() { @Override public void onSuccess(final @NonNull Pair result) { try { boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms(); int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1); long expiresIn = recipient.getExpireMessages() * 1000; boolean initiating = threadId == -1; AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first, result.second, MediaUtil.AUDIO_AAC, true); SlideDeck slideDeck = new SlideDeck(); slideDeck.addSlide(audioSlide); sendMediaMessage(forceSms, "", slideDeck, expiresIn, subscriptionId, initiating).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Void nothing) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { PersistentBlobProvider.getInstance(ConversationActivity.this).delete(result.first); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } }); } catch (InvalidMessageException e) { Log.w(TAG, e); Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_error_sending_voice_message, Toast.LENGTH_LONG).show(); } } @Override public void onFailure(ExecutionException e) { Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show(); } }); } @Override public void onRecorderCanceled() { Vibrator vibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE); vibrator.vibrate(50); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); ListenableFuture> future = audioRecorder.stopRecording(); future.addListener(new ListenableFuture.Listener>() { @Override public void onSuccess(final Pair result) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { PersistentBlobProvider.getInstance(ConversationActivity.this).delete(result.first); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @Override public void onFailure(ExecutionException e) {} }); } @Override public void onEmojiToggle() { if (!emojiDrawerStub.resolved()) { inputPanel.setEmojiDrawer(emojiDrawerStub.get()); emojiDrawerStub.get().setEmojiEventListener(inputPanel); } if (container.getCurrentInput() == emojiDrawerStub.get()) { container.showSoftkey(composeText); } else { container.show(composeText, emojiDrawerStub.get()); } } @Override public void onMediaSelected(@NonNull Uri uri, String contentType) { if (!TextUtils.isEmpty(contentType) && contentType.trim().equals("image/gif")) { setMedia(uri, MediaType.GIF); } else if (MediaUtil.isImageType(contentType)) { setMedia(uri, MediaType.IMAGE); } else if (MediaUtil.isVideoType(contentType)) { setMedia(uri, MediaType.VIDEO); } else if (MediaUtil.isAudioType(contentType)) { setMedia(uri, MediaType.AUDIO); } } // Listeners private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener { @Override public void onClick(int type) { addAttachment(type); } @Override public void onQuickAttachment(Uri uri) { Intent intent = new Intent(); intent.setData(uri); onActivityResult(PICK_GALLERY, RESULT_OK, intent); } } private class QuickCameraToggleListener implements OnClickListener { @Override public void onClick(View v) { if (!quickAttachmentDrawer.isShowing()) { composeText.clearFocus(); container.show(composeText, quickAttachmentDrawer); } else { container.hideAttachedInput(false); } } } 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 AttachButtonLongClickListener implements View.OnLongClickListener { @Override public boolean onLongClick(View v) { return sendButton.performLongClick(); } } 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) { container.showSoftkey(composeText); } @Override public void beforeTextChanged(CharSequence s, int start, int count,int after) { beforeLength = composeText.getTextTrimmed().length(); } @Override public void afterTextChanged(Editable s) { calculateCharactersRemaining(); if (composeText.getTextTrimmed().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) {} } @Override public void setThreadId(long threadId) { this.threadId = threadId; } @Override public void onAttachmentChanged() { handleSecurityChange(isSecureText, isDefaultSms); updateToggleButtonState(); } private class UnverifiedDismissedListener implements UnverifiedBannerView.DismissListener { @Override public void onDismissed(final List unverifiedIdentities) { final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ConversationActivity.this); new AsyncTask() { @Override protected Void doInBackground(Void... params) { synchronized (SESSION_LOCK) { for (IdentityRecord identityRecord : unverifiedIdentities) { identityDatabase.setVerified(identityRecord.getAddress(), identityRecord.getIdentityKey(), VerifiedStatus.DEFAULT); } } return null; } @Override protected void onPostExecute(Void result) { initializeIdentityRecords(); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } private class UnverifiedClickedListener implements UnverifiedBannerView.ClickListener { @Override public void onClicked(final List unverifiedIdentities) { Log.w(TAG, "onClicked: " + unverifiedIdentities.size()); if (unverifiedIdentities.size() == 1) { Intent intent = new Intent(ConversationActivity.this, VerifyIdentityActivity.class); intent.putExtra(VerifyIdentityActivity.ADDRESS_EXTRA, unverifiedIdentities.get(0).getAddress()); intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(unverifiedIdentities.get(0).getIdentityKey())); intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, false); startActivity(intent); } else { String[] unverifiedNames = new String[unverifiedIdentities.size()]; for (int i=0;i