/* * 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.conversation; import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.hardware.Camera; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Vibrator; import android.provider.Browser; import android.provider.Telephony; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Pair; import android.util.TypedValue; 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.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; import androidx.core.view.MenuItemCompat; import androidx.lifecycle.ViewModelProviders; import androidx.loader.app.LoaderManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import com.annimon.stream.Stream; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.session.libsession.messaging.mentions.MentionsManager; import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate; import org.session.libsession.messaging.messages.visible.VisibleMessage; import org.session.libsession.messaging.open_groups.OpenGroup; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.threads.DistributionTypes; import org.session.libsession.utilities.GroupUtil; import org.session.libsession.utilities.MediaTypes; import org.session.libsignal.libsignal.InvalidMessageException; import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.service.loki.utilities.mentions.Mention; import org.session.libsignal.service.loki.utilities.HexEncodingKt; import org.session.libsignal.service.loki.utilities.PublicKeyValidation; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ExpirationDialog; import org.thoughtcrime.securesms.MediaOverviewActivity; import org.thoughtcrime.securesms.MuteDialog; import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; import org.thoughtcrime.securesms.ShortcutLauncherActivity; import org.thoughtcrime.securesms.audio.AudioRecorder; import org.thoughtcrime.securesms.audio.AudioSlidePlayer; import org.thoughtcrime.securesms.components.AnimatingToggle; import org.thoughtcrime.securesms.components.AttachmentTypeSelector; import org.thoughtcrime.securesms.components.ComposeText; import org.thoughtcrime.securesms.components.ConversationSearchBottomBar; 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.emoji.EmojiKeyboardProvider; import org.thoughtcrime.securesms.components.emoji.EmojiStrings; import org.thoughtcrime.securesms.components.emoji.MediaKeyboard; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; import org.thoughtcrime.securesms.contactshare.ContactUtil; import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher; import org.session.libsession.messaging.threads.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.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MmsSmsColumns.Types; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel; import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity; import org.thoughtcrime.securesms.loki.activities.HomeActivity; import org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt; import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities; import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities; import org.thoughtcrime.securesms.loki.views.MentionCandidateSelectionView; import org.thoughtcrime.securesms.loki.views.ProfilePictureView; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.MediaSendActivity; import org.thoughtcrime.securesms.mms.AttachmentManager; import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType; import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.GifSlide; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MmsException; import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.QuoteId; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.TextSlide; import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.providers.BlobProvider; import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.messaging.threads.recipients.RecipientFormattingException; import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.search.model.MessageResult; import org.session.libsession.messaging.sending_receiving.MessageSender; import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.PushCharacterCalculator; import org.session.libsession.utilities.ServiceUtil; import org.session.libsession.utilities.Util; import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; import org.session.libsession.messaging.threads.GroupRecord; import org.session.libsession.utilities.ExpirationUtil; import org.session.libsession.utilities.views.Stub; import org.session.libsession.utilities.ViewUtil; import org.session.libsession.utilities.concurrent.AssertedSuccessListener; import org.session.libsignal.utilities.concurrent.ListenableFuture; import org.session.libsignal.utilities.concurrent.SettableFuture; import org.session.libsession.utilities.TextSecurePreferences; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import kotlin.Unit; import network.loki.messenger.R; /** * 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, InputPanel.Listener, InputPanel.MediaListener, ComposeText.CursorPositionChangedListener, ConversationSearchBottomBar.EventListener { 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 MEDIA_EXTRA = "media_list"; public static final String STICKER_EXTRA = "media_list"; 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"; public static final String STARTING_POSITION_EXTRA = "starting_position"; // 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 = 4; // private static final int GET_CONTACT_DETAILS = 5; // private static final int GROUP_EDIT = 6; private static final int TAKE_PHOTO = 7; private static final int ADD_CONTACT = 8; private static final int PICK_LOCATION = 9; private static final int PICK_GIF = 10; private static final int SMS_DEFAULT = 11; private static final int MEDIA_SENDER = 12; private GlideRequests glideRequests; protected ComposeText composeText; private AnimatingToggle buttonToggle; private ImageButton sendButton; private ImageButton attachButton; private ProfilePictureView profilePictureView; private TextView titleTextView; private ConversationFragment fragment; private Button unblockButton; private Button makeDefaultSmsButton; private InputAwareLayout container; private TypingStatusTextWatcher typingTextWatcher; private MentionTextWatcher mentionTextWatcher; private ConversationSearchBottomBar searchNav; private MenuItem searchViewItem; private ProgressBar messageStatusProgressBar; private ImageView muteIndicatorImageView; private TextView subtitleTextView; private View homeButtonContainer; private AttachmentTypeSelector attachmentTypeSelector; private AttachmentManager attachmentManager; private AudioRecorder audioRecorder; private Handler audioHandler; private Runnable stopRecordingTask; private Stub emojiDrawerStub; protected HidingLinearLayout quickAttachmentToggle; protected HidingLinearLayout inlineAttachmentToggle; private InputPanel inputPanel; private LinkPreviewViewModel linkPreviewViewModel; private ConversationSearchViewModel searchViewModel; private Recipient recipient; private long threadId; private int distributionType; private boolean isDefaultSms = false; private boolean isSecurityInitialized = false; private int expandedKeyboardHeight = 0; private int collapsedKeyboardHeight = Integer.MAX_VALUE; private int keyboardHeight = 0; // Message status bar private ArrayList broadcastReceivers = new ArrayList<>(); private String messageStatus = null; // Mentions private View mentionCandidateSelectionViewContainer; private MentionCandidateSelectionView mentionCandidateSelectionView; private int currentMentionStartIndex = -1; private ArrayList mentions = new ArrayList<>(); private String oldText = ""; private final PushCharacterCalculator characterCalculator = new PushCharacterCalculator(); @Override protected void onCreate(Bundle state, boolean ready) { Log.i(TAG, "onCreate()"); setContentView(R.layout.conversation_activity); fragment = initFragment(R.id.fragment_content, new ConversationFragment(), Locale.getDefault()); registerMessageStatusObserver("calculatingPoW"); registerMessageStatusObserver("contactingNetwork"); registerMessageStatusObserver("sendingMessage"); registerMessageStatusObserver("messageSent"); registerMessageStatusObserver("messageFailed"); BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Toast.makeText(ConversationActivity.this, "Your clock is out of sync with the service node network.", Toast.LENGTH_LONG).show(); } }; broadcastReceivers.add(broadcastReceiver); LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, new IntentFilter("clockOutOfSync")); initializeActionBar(); initializeViews(); initializeResources(); initializeLinkPreviewObserver(); initializeSearchObserver(); initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean result) { initializeDraft().addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean loadedDraft) { if (loadedDraft != null && loadedDraft) { Log.i(TAG, "Finished loading draft"); Util.runOnMain(() -> { if (fragment != null && fragment.isResumed()) { fragment.moveToLastSeen(); } else { Log.w(TAG, "Wanted to move to the last seen position, but the fragment was in an invalid state"); } }); } if (TextSecurePreferences.isTypingIndicatorsEnabled(ConversationActivity.this)) { composeText.addTextChangedListener(typingTextWatcher); } composeText.setSelection(composeText.length(), composeText.length()); composeText.addTextChangedListener(mentionTextWatcher); mentionCandidateSelectionView.setGlide(glideRequests); mentionCandidateSelectionView.setOnMentionCandidateSelected( mentionCandidate -> { mentions.add(mentionCandidate); String oldText = composeText.getText().toString(); String newText = oldText.substring(0, currentMentionStartIndex) + "@" + mentionCandidate.getDisplayName() + " "; composeText.setText(newText); composeText.setSelection(newText.length()); currentMentionStartIndex = -1; mentionCandidateSelectionView.hide(); ConversationActivity.this.oldText = newText; return Unit.INSTANCE; }); } }); } }); MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this); OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId); if (publicChat != null) { // Request open group info update and handle the successful result in #onOpenGroupInfoUpdated(). PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel()); } View rootView = findViewById(R.id.rootView); rootView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { int height = rootView.getRootView().getHeight() - rootView.getHeight(); int thresholdInDP = 120; float scale = getResources().getDisplayMetrics().density; int thresholdInPX = (int)(thresholdInDP * scale); if (expandedKeyboardHeight == 0 || height > thresholdInPX) { expandedKeyboardHeight = height; } collapsedKeyboardHeight = Math.min(collapsedKeyboardHeight, height); keyboardHeight = expandedKeyboardHeight - collapsedKeyboardHeight; // Use 300dp if the keyboard wasn't opened yet. if (keyboardHeight == 0) { keyboardHeight = (int)(300f * getResources().getDisplayMetrics().density); } }); } private void registerMessageStatusObserver(String status) { BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { long timestamp = intent.getLongExtra("long", 0); handleMessageStatusChanged(status, timestamp); } }; broadcastReceivers.add(broadcastReceiver); LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, new IntentFilter(status)); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); Log.i(TAG, "onNewIntent()"); if (isFinishing()) { Log.w(TAG, "Activity is finishing..."); return; } if (!org.thoughtcrime.securesms.util.Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent()) { saveDraft(); attachmentManager.clear(glideRequests, false); silentlySetComposeText(""); } setIntent(intent); initializeResources(); initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean result) { initializeDraft(); } }); if (fragment != null) { fragment.onNewIntent(); } searchNav.setVisibility(View.GONE); } @Override protected void onResume() { super.onResume(); EventBus.getDefault().register(this); initializeEnabledCheck(); composeText.setTransport(); updateTitleTextView(recipient); updateProfilePicture(); updateSubtitleTextView(); updateInputUI(recipient); ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(threadId); markThreadAsRead(); inputPanel.setHint(getResources().getString(R.string.ConversationActivity_message)); Log.i(TAG, "onResume() Finished: " + (System.currentTimeMillis() - getIntent().getLongExtra(TIMING_EXTRA, 0))); } @Override protected void onPause() { super.onPause(); ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(-1L); if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right); 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.i(TAG, "onConfigurationChanged(" + newConfig.orientation + ")"); super.onConfigurationChanged(newConfig); composeText.setTransport(); if (emojiDrawerStub.resolved() && container.getCurrentInput() == emojiDrawerStub.get()) { container.hideAttachedInput(true); } } @Override protected void onDestroy() { saveDraft(); if (recipient != null) recipient.removeListener(this); for (BroadcastReceiver broadcastReceiver : broadcastReceivers) { LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver); } super.onDestroy(); } @Override public void onActivityResult(final int reqCode, int resultCode, Intent data) { Log.i(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)) { updateLinkPreviewState(); return; } switch (reqCode) { case PICK_DOCUMENT: setMedia(data.getData(), MediaType.DOCUMENT); break; case PICK_AUDIO: setMedia(data.getData(), MediaType.AUDIO); 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(place, getCurrentMediaConstraints()); break; */ case PICK_GIF: setMedia(data.getData(), MediaType.GIF, data.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0), data.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0)); break; case SMS_DEFAULT: initializeSecurity(true, isDefaultSms); break; case MEDIA_SENDER: long expiresIn = recipient.getExpireMessages() * 1000L; int subscriptionId = -1; boolean initiating = threadId == -1; String message = data.getStringExtra(MediaSendActivity.EXTRA_MESSAGE); SlideDeck slideDeck = new SlideDeck(); List mediaList = data.getParcelableArrayListExtra(MediaSendActivity.EXTRA_MEDIA); for (Media mediaItem : mediaList) { if (MediaUtil.isVideoType(mediaItem.getMimeType())) { slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), 0, mediaItem.getCaption().orNull())); } else if (MediaUtil.isGif(mediaItem.getMimeType())) { slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull())); } else if (MediaUtil.isImageType(mediaItem.getMimeType())) { slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull())); } else { Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping."); } } final Context context = ConversationActivity.this.getApplicationContext(); sendMediaMessage(message, slideDeck, inputPanel.getQuote().orNull(), Optional.absent(), initiating).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Void result) { AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { Stream.of(slideDeck.getSlides()) .map(Slide::getUri) .withoutNulls() .filter(BlobProvider::isAuthority) .forEach(uri -> BlobProvider.getInstance().delete(context, uri)); }); } }); 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(); boolean isOpenGroupOrRSSFeed = recipient.getAddress().isOpenGroup(); if (!isOpenGroupOrRSSFeed) { 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 ImageView iconView = actionView.findViewById(R.id.menu_badge_icon); final TextView badgeView = actionView.findViewById(R.id.expiration_badge); @ColorInt int color = GeneralUtilitiesKt.getColorWithID(getResources(), R.color.text, getTheme()); iconView.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); badgeView.setText(ExpirationUtil.getExpirationAbbreviatedDisplayValue(this, recipient.getExpireMessages())); actionView.setOnClickListener(v -> onOptionsItemSelected(item)); } else { inflater.inflate(R.menu.conversation_expiring_off, menu); } } if (isSingleConversation()) { if (recipient.isBlocked()) { inflater.inflate(R.menu.conversation_unblock, menu); } else { inflater.inflate(R.menu.conversation_block, menu); } inflater.inflate(R.menu.conversation_copy_session_id, menu); } else if (isGroupConversation() && !isOpenGroupOrRSSFeed) { // inflater.inflate(R.menu.conversation_group_options, menu); if (!isPushGroupConversation()) { inflater.inflate(R.menu.conversation_mms_group_options, menu); if (distributionType == 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()) { // inflater.inflate(R.menu.conversation_secure, 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); } if (recipient != null && recipient.isLocalNumber()) { if (isSecureText) menu.findItem(R.id.menu_call_secure).setVisible(false); else menu.findItem(R.id.menu_call_insecure).setVisible(false); MenuItem muteItem = menu.findItem(R.id.menu_mute_notifications); if (muteItem != null) { muteItem.setVisible(false); } } */ searchViewItem = menu.findItem(R.id.menu_search); SearchView searchView = (SearchView)searchViewItem.getActionView(); SearchView.OnQueryTextListener queryListener = new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { searchViewModel.onQueryUpdated(query, threadId); searchNav.showLoading(); fragment.onSearchQueryUpdated(query); return true; } @Override public boolean onQueryTextChange(String query) { searchViewModel.onQueryUpdated(query, threadId); searchNav.showLoading(); fragment.onSearchQueryUpdated(query); return true; } }; searchViewItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { @Override public boolean onMenuItemActionExpand(MenuItem item) { searchView.setOnQueryTextListener(queryListener); searchViewModel.onSearchOpened(); searchNav.setVisibility(View.VISIBLE); searchNav.setData(0, 0); inputPanel.setVisibility(View.GONE); for (int i = 0; i < menu.size(); i++) { if (!menu.getItem(i).equals(searchViewItem)) { menu.getItem(i).setVisible(false); } } return true; } @Override public boolean onMenuItemActionCollapse(MenuItem item) { searchView.setOnQueryTextListener(null); searchViewModel.onSearchClosed(); searchNav.setVisibility(View.GONE); inputPanel.setVisibility(View.VISIBLE); updateInputUI(recipient); fragment.onSearchQueryUpdated(null); invalidateOptionsMenu(); return true; } }); super.onPrepareOptionsMenu(menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { super.onOptionsItemSelected(item); switch (item.getItemId()) { // case R.id.menu_call_secure: handleDial(getRecipient(), true); return true; // case R.id.menu_call_insecure: handleDial(getRecipient(), false); return true; case R.id.menu_unblock: handleUnblock(); return true; case R.id.menu_block: handleBlock(); return true; case R.id.menu_copy_session_id: handleCopySessionID(); return true; case R.id.menu_view_media: handleViewMedia(); return true; case R.id.menu_add_shortcut: handleAddShortcut(); return true; case R.id.menu_search: handleSearch(); 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_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.d(TAG, "onBackPressed()"); if (container.isInputOpen()) container.hideCurrentInput(composeText); else super.onBackPressed(); } @Override public void onKeyboardShown() { inputPanel.onKeyboardShown(); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } //////// Event Handlers private void handleReturnToConversationList() { Intent intent = new Intent(this, HomeActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); finish(); } private void handleSelectMessageExpiration() { if (isPushGroupConversation() && !isActiveGroup()) { return; } //noinspection CodeBlock2Expr ExpirationDialog.show(this, recipient.getExpireMessages(), expirationTime -> { new AsyncTask() { @Override protected Void doInBackground(Void... params) { DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient, expirationTime); ExpirationTimerUpdate message = new ExpirationTimerUpdate(expirationTime); message.setRecipient(recipient.getAddress().serialize()); // we need the recipient in ExpiringMessageManager.insertOutgoingExpirationTimerMessage message.setSentTimestamp(System.currentTimeMillis()); ExpiringMessageManager expiringMessageManager = ApplicationContext.getInstance(getApplicationContext()).getExpiringMessageManager(); expiringMessageManager.setExpirationTimer(message); MessageSender.send(message, recipient.getAddress()); 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, 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 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() { int titleRes = R.string.ConversationActivity_unblock_this_contact_question; int bodyRes = R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact; new AlertDialog.Builder(this) .setTitle(titleRes) .setMessage(bodyRes) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.ConversationActivity_unblock, (dialog, which) -> { new AsyncTask() { @Override protected Void doInBackground(Void... params) { DatabaseFactory.getRecipientDatabase(ConversationActivity.this) .setBlocked(recipient, false); 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 handleBlock() { int titleRes = R.string.RecipientPreferenceActivity_block_this_contact_question; int bodyRes = R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact; new AlertDialog.Builder(this) .setTitle(titleRes) .setMessage(bodyRes) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.RecipientPreferenceActivity_block, (dialog, which) -> { new AsyncTask() { @Override protected Void doInBackground(Void... params) { DatabaseFactory.getRecipientDatabase(ConversationActivity.this) .setBlocked(recipient, true); Util.runOnMain(() -> finish()); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); }).show(); } private void handleCopySessionID() { if (recipient.isGroupRecipient()) { return; } String sessionID = recipient.getAddress().toString(); ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("Session ID", sessionID); clipboard.setPrimaryClip(clip); Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); } private void handleViewMedia() { Intent intent = new Intent(this, MediaOverviewActivity.class); intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, recipient.getAddress()); startActivity(intent); } private void handleAddShortcut() { Log.i(TAG, "Creating home screen shortcut for recipient " + recipient.getAddress()); new AsyncTask() { @Override protected IconCompat doInBackground(Void... voids) { Context context = getApplicationContext(); IconCompat icon = null; if (recipient.getContactPhoto() != null) { try { Bitmap bitmap = BitmapFactory.decodeStream(recipient.getContactPhoto().openInputStream(context)); bitmap = BitmapUtil.createScaledBitmap(bitmap, 300, 300); icon = IconCompat.createWithAdaptiveBitmap(bitmap); } catch (IOException e) { Log.w(TAG, "Failed to decode contact photo during shortcut creation. Falling back to generic icon.", e); } } if (icon == null) { icon = IconCompat.createWithResource(context, recipient.isGroupRecipient() ? R.mipmap.ic_group_shortcut : R.mipmap.ic_person_shortcut); } return icon; } @Override protected void onPostExecute(IconCompat icon) { Context context = getApplicationContext(); String name = Optional.fromNullable(recipient.getName()) .or(Optional.fromNullable(recipient.getProfileName())) .or(recipient.toShortString()); ShortcutInfoCompat shortcutInfo = new ShortcutInfoCompat.Builder(context, recipient.getAddress().serialize() + '-' + System.currentTimeMillis()) .setShortLabel(name) .setIcon(icon) .setIntent(ShortcutLauncherActivity.createIntent(context, recipient.getAddress())) .build(); if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)) { Toast.makeText(context, getString(R.string.ConversationActivity_added_to_home_screen), Toast.LENGTH_LONG).show(); } } }.execute(); } private void handleSearch() { searchViewModel.onSearchOpened(); } 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); GroupRecord group = DatabaseFactory.getGroupDatabase(this).getGroup(getRecipient().getAddress().toGroupString()).orNull(); List
admins = group.getAdmins(); String userPublicKey = TextSecurePreferences.getLocalNumber(this); String message = getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group); for (Address admin : admins) { if (admin.toString().equals(userPublicKey)) { message = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone."; } } builder.setMessage(message); builder.setPositiveButton(R.string.yes, (dialog, which) -> { Recipient groupRecipient = getRecipient(); String groupPublicKey; boolean isClosedGroup; try { groupPublicKey = HexEncodingKt.toHexString(GroupUtil.doubleDecodeGroupID(groupRecipient.getAddress().toString())); isClosedGroup = DatabaseFactory.getLokiAPIDatabase(this).isClosedGroup(groupPublicKey); } catch (IOException e) { groupPublicKey = null; isClosedGroup = false; } try { if (isClosedGroup) { MessageSender.explicitLeave(groupPublicKey, true); initializeEnabledCheck(); } else { Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show(); } } catch (Exception e) { Toast.makeText(this, 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(this, EditClosedGroupActivity.class); String groupID = this.recipient.getAddress().toGroupString(); intent.putExtra(EditClosedGroupActivity.Companion.getGroupIDKey(), groupID); startActivity(intent); } private void handleDistributionBroadcastEnabled(MenuItem item) { distributionType = DistributionTypes.BROADCAST; item.setChecked(true); if (threadId != -1) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { DatabaseFactory.getThreadDatabase(ConversationActivity.this) .setDistributionType(threadId, DistributionTypes.BROADCAST); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } private void handleDistributionConversationEnabled(MenuItem item) { distributionType = DistributionTypes.CONVERSATION; item.setChecked(true); if (threadId != -1) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { DatabaseFactory.getThreadDatabase(ConversationActivity.this) .setDistributionType(threadId, DistributionTypes.CONVERSATION); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } private void handleAddAttachment() { if (attachmentTypeSelector == null) { attachmentTypeSelector = new AttachmentTypeSelector( this, LoaderManager.getInstance(this), new AttachmentTypeListener(), keyboardHeight); } attachmentTypeSelector.show(this, attachButton); } private void handleSecurityChange(boolean isSecureText, boolean isDefaultSms) { Log.i(TAG, "handleSecurityChange(" + isSecureText + ", " + isDefaultSms + ")"); if (isSecurityInitialized && isSecureText == true && isDefaultSms == this.isDefaultSms) { return; } this.isDefaultSms = isDefaultSms; this.isSecurityInitialized = true; if (recipient == null || attachmentManager == null) { return; } boolean isMediaMessage = recipient.isMmsGroupRecipient() || attachmentManager.isAttachmentPresent(); /* Loki - We don't support SMS if (!isSecureText && !isPushGroupConversation()) sendButton.disableTransport(Type.TEXTSECURE); if (recipient.isPushGroupRecipient()) sendButton.disableTransport(Type.SMS); if (!recipient.isPushGroupRecipient() && recipient.isForceSmsSelection()) { sendButton.setDefaultTransport(Type.SMS); } else { if (isSecureText || isPushGroupConversation()) sendButton.setDefaultTransport(Type.TEXTSECURE); else sendButton.setDefaultTransport(Type.SMS); } */ supportInvalidateOptionsMenu(); updateInputUI(recipient); } ///// Initializers private ListenableFuture initializeDraft() { final SettableFuture result = new SettableFuture<>(); final String draftText = getIntent().getStringExtra(TEXT_EXTRA); final Uri draftMedia = getIntent().getData(); final MediaType draftMediaType = MediaType.from(getIntent().getType()); final List mediaList = getIntent().getParcelableArrayListExtra(MEDIA_EXTRA); if (!Util.isEmpty(mediaList)) { Intent sendIntent = MediaSendActivity.buildEditorIntent(this, mediaList, recipient, draftText); startActivityForResult(sendIntent, MEDIA_SENDER); return new SettableFuture<>(false); } if (draftText != null) { composeText.setText(""); composeText.append(draftText); result.set(true); } if (draftMedia != null && draftMediaType != null) { return setMedia(draftMedia, draftMediaType); } if (draftText == null && draftMedia == null && draftMediaType == null) { return initializeDraftFromDatabase(); } else { updateToggleButtonState(); result.set(false); } return result; } private void initializeEnabledCheck() { boolean enabled = !(isPushGroupConversation() && !isActiveGroup()); inputPanel.setEnabled(enabled); sendButton.setEnabled(enabled); attachButton.setEnabled(enabled); } private ListenableFuture initializeDraftFromDatabase() { SettableFuture future = new SettableFuture<>(); new AsyncTask>() { @Override protected List doInBackground(Void... params) { DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this); List results = draftDatabase.getDrafts(threadId); draftDatabase.clearDrafts(threadId); return results; } @Override protected void onPostExecute(List drafts) { if (drafts.isEmpty()) { future.set(false); updateToggleButtonState(); return; } AtomicInteger draftsRemaining = new AtomicInteger(drafts.size()); AtomicBoolean success = new AtomicBoolean(false); ListenableFuture.Listener listener = new AssertedSuccessListener() { @Override public void onSuccess(Boolean result) { success.compareAndSet(false, result); if (draftsRemaining.decrementAndGet() <= 0) { future.set(success.get()); } } }; for (Draft draft : drafts) { switch (draft.getType()) { case Draft.TEXT: composeText.setText(draft.getValue()); listener.onSuccess(true); break; case Draft.IMAGE: setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE).addListener(listener); break; case Draft.AUDIO: setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO).addListener(listener); break; case Draft.VIDEO: setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO).addListener(listener); break; case Draft.QUOTE: SettableFuture quoteResult = new SettableFuture<>(); new QuoteRestorationTask(draft.getValue(), quoteResult).execute(); quoteResult.addListener(listener); break; } } updateToggleButtonState(); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); return future; } 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) { // Loki - Override the flag below boolean signalEnabled = true; // TextSecurePreferences.isPushRegistered(context); return new boolean[] { signalEnabled, false}; } @Override protected void onPostExecute(boolean[] result) { if (result[0] != currentSecureText || result[1] != currentIsDefaultSms) { Log.i(TAG, "onPostExecute() handleSecurityChange: " + result[0] + " , " + result[1]); handleSecurityChange(result[0], result[1]); } future.set(true); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, recipient); return future; } private void initializeViews() { profilePictureView = findViewById(R.id.profilePictureView); titleTextView = findViewById(R.id.titleTextView); 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); 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); container = ViewUtil.findById(this, R.id.layout_container); quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle); inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container); inputPanel = ViewUtil.findById(this, R.id.bottom_panel); searchNav = ViewUtil.findById(this, R.id.conversation_search_nav); mentionCandidateSelectionViewContainer = ViewUtil.findById(this, R.id.mentionCandidateSelectionViewContainer); mentionCandidateSelectionView = ViewUtil.findById(this, R.id.userSelectionView); messageStatusProgressBar = ViewUtil.findById(this, R.id.messageStatusProgressBar); muteIndicatorImageView = ViewUtil.findById(this, R.id.muteIndicatorImageView); subtitleTextView = ViewUtil.findById(this, R.id.subtitleTextView); homeButtonContainer = ViewUtil.findById(this, R.id.homeButtonContainer); ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle); ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button); container.addOnKeyboardShownListener(this); inputPanel.setListener(this); inputPanel.setMediaListener(this); attachmentTypeSelector = null; attachmentManager = new AttachmentManager(this, this); audioRecorder = new AudioRecorder(this); typingTextWatcher = new TypingStatusTextWatcher(); mentionTextWatcher = new MentionTextWatcher(); SendButtonListener sendButtonListener = new SendButtonListener(); ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener(); composeText.setOnEditorActionListener(sendButtonListener); composeText.setCursorPositionChangedListener(this); attachButton.setOnClickListener(new AttachButtonListener()); attachButton.setOnLongClickListener(new AttachButtonLongClickListener()); sendButton.setOnClickListener(sendButtonListener); sendButton.setEnabled(true); unblockButton.setOnClickListener(v -> handleUnblock()); makeDefaultSmsButton.setOnClickListener(v -> handleMakeDefaultSms()); composeText.setOnKeyListener(composeKeyPressedListener); composeText.addTextChangedListener(composeKeyPressedListener); composeText.setOnEditorActionListener(sendButtonListener); composeText.setOnClickListener(composeKeyPressedListener); composeText.setOnFocusChangeListener(composeKeyPressedListener); if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA) && Camera.getNumberOfCameras() > 0) { quickCameraToggle.setVisibility(View.VISIBLE); quickCameraToggle.setOnClickListener(new QuickCameraToggleListener()); } else { quickCameraToggle.setVisibility(View.GONE); } searchNav.setEventListener(this); inlineAttachmentButton.setOnClickListener(v -> handleAddAttachment()); homeButtonContainer.setOnClickListener(v -> onSupportNavigateUp()); } protected void initializeActionBar() { Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar supportActionBar = getSupportActionBar(); if (supportActionBar == null) throw new AssertionError(); // supportActionBar.setDisplayHomeAsUpEnabled(true); supportActionBar.setDisplayShowTitleEnabled(false); } private void initializeResources() { if (recipient != null) recipient.removeListener(this); Address address = getIntent().getParcelableExtra(ADDRESS_EXTRA); if (address == null) { finish(); return; } recipient = Recipient.from(this, address, true); threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1); distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT); glideRequests = GlideApp.with(this); recipient.addListener(this); } private void initializeLinkPreviewObserver() { linkPreviewViewModel = ViewModelProviders.of(this, new LinkPreviewViewModel.Factory(new LinkPreviewRepository(this))).get(LinkPreviewViewModel.class); if (!TextSecurePreferences.isLinkPreviewsEnabled(this)) { linkPreviewViewModel.onUserCancel(); return; } linkPreviewViewModel.getLinkPreviewState().observe(this, previewState -> { if (previewState == null) return; if (previewState.isLoading()) { Log.d(TAG, "Loading link preview."); inputPanel.setLinkPreviewLoading(); } else { Log.d(TAG, "Setting link preview: " + previewState.getLinkPreview().isPresent()); inputPanel.setLinkPreview(glideRequests, previewState.getLinkPreview()); } updateToggleButtonState(); }); } private void initializeSearchObserver() { searchViewModel = ViewModelProviders.of(this).get(ConversationSearchViewModel.class); searchViewModel.getSearchResults().observe(this, result -> { if (result == null) return; if (!result.getResults().isEmpty()) { MessageResult messageResult = result.getResults().get(result.getPosition()); fragment.jumpToMessage(messageResult.messageRecipient.getAddress(), messageResult.receivedTimestampMs, searchViewModel::onMissingResult); } searchNav.setData(result.getPosition(), result.getResults().size()); }); } @Override public void onSearchMoveUpPressed() { searchViewModel.onMoveUp(); } @Override public void onSearchMoveDownPressed() { searchViewModel.onMoveDown(); } @Override public void onModified(final Recipient recipient) { Log.i(TAG, "onModified(" + recipient.getAddress().serialize() + ")"); Util.runOnMain(() -> { Log.i(TAG, "onModifiedRun(): " + recipient.getRegistered()); updateTitleTextView(recipient); updateProfilePicture(); updateSubtitleTextView(); updateInputUI(recipient); initializeSecurity(true, isDefaultSms); if (searchViewItem == null || !searchViewItem.isActionViewExpanded()) { invalidateOptionsMenu(); } }); } @Subscribe(threadMode = ThreadMode.MAIN) public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) { OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId); if (publicChat != null && publicChat.getChannel() == event.getChannel() && publicChat.getServer().equals(event.getUrl())) { this.updateSubtitleTextView(); } } //////// Helper Methods private void addAttachment(int type) { linkPreviewViewModel.onUserCancel(); Log.i(TAG, "Selected: " + type); switch (type) { case AttachmentTypeSelector.ADD_GALLERY: AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient, composeText.getTextTrimmed()); 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); 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: boolean hasSeenGIFMetaDataWarning = TextSecurePreferences.hasSeenGIFMetaDataWarning(this); if (!hasSeenGIFMetaDataWarning) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("Search GIFs?"); builder.setMessage("You will not have full metadata protection when sending GIFs."); builder.setPositiveButton("OK", (dialog, which) -> { AttachmentManager.selectGif(this, PICK_GIF); dialog.dismiss(); }); builder.setNegativeButton("Cancel", (dialog, which) -> dialog.dismiss()); builder.create().show(); TextSecurePreferences.setHasSeenGIFMetaDataWarning(this); } else { AttachmentManager.selectGif(this, PICK_GIF); } break; } } private ListenableFuture setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) { return setMedia(uri, mediaType, 0, 0); } private ListenableFuture setMedia(@Nullable Uri uri, @NonNull MediaType mediaType, int width, int height) { if (uri == null) { return new SettableFuture<>(false); } if (MediaType.VCARD.equals(mediaType)) { return new SettableFuture<>(false); } else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) { Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, 0, Optional.absent(), Optional.absent()); startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed()), MEDIA_SENDER); return new SettableFuture<>(false); } else { return attachmentManager.setMedia(glideRequests, uri, mediaType, MediaConstraints.getPushMediaConstraints(), width, height); } } 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, (dialog, which) -> composeText.append(numbers[which])); builder.show(); } private Drafts getDraftsForCurrentState() { Drafts drafts = new Drafts(); if (!org.thoughtcrime.securesms.util.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.hasImage() && slide.getUri() != null) drafts.add(new Draft(Draft.IMAGE, slide.getUri().toString())); } Optional quote = inputPanel.getQuote(); if (quote.isPresent()) { drafts.add(new Draft(Draft.QUOTE, new QuoteId(quote.get().getId(), quote.get().getAuthor()).serialize())); } 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 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.getOrCreateThreadIdFor(getRecipient(), thisDistributionType); draftDatabase.insertDrafts(threadId, drafts); threadDatabase.updateSnippet(threadId, drafts.getSnippet(ConversationActivity.this), drafts.getUriSnippet(), 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 updateInputUI(Recipient recipient) { if (recipient.isGroupRecipient() && !isActiveGroup()) { unblockButton.setVisibility(View.GONE); inputPanel.setVisibility(View.GONE); makeDefaultSmsButton.setVisibility(View.GONE); } else if (recipient.isBlocked()) { unblockButton.setVisibility(View.VISIBLE); inputPanel.setVisibility(View.GONE); makeDefaultSmsButton.setVisibility(View.GONE); } else { inputPanel.setVisibility(View.VISIBLE); unblockButton.setVisibility(View.GONE); makeDefaultSmsButton.setVisibility(View.GONE); } } private void initializeMediaKeyboardProviders(@NonNull MediaKeyboard mediaKeyboard) { boolean isSystemEmojiPreferred = TextSecurePreferences.isSystemEmojiPreferred(this); if (!isSystemEmojiPreferred) { mediaKeyboard.setProviders(0, new EmojiKeyboardProvider(this, inputPanel)); } } 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(); } @SuppressWarnings("SimplifiableIfStatement") private boolean isSelfConversation() { if (!TextSecurePreferences.isPushRegistered(this)) return false; if (recipient.isGroupRecipient()) return false; return Util.isOwnNumber(this, recipient.getAddress().serialize()); } 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 result = composeText.getTextTrimmed(); if (result.length() < 1 && !attachmentManager.isAttachmentPresent()) throw new InvalidMessageException(); for (Mention mention : mentions) { try { int startIndex = result.indexOf("@" + mention.getDisplayName()); int endIndex = startIndex + mention.getDisplayName().length() + 1; // + 1 to include the @ result = result.substring(0, startIndex) + "@" + mention.getPublicKey() + result.substring(endIndex); } catch (Exception exception) { Log.d("Loki", "Couldn't process mention due to error: " + exception.toString() + "."); } } return result; } private Pair> getSplitMessage(String rawText, int maxPrimaryMessageSize) { String bodyText = rawText; Optional textSlide = Optional.absent(); if (bodyText.length() > maxPrimaryMessageSize) { bodyText = rawText.substring(0, maxPrimaryMessageSize); byte[] textData = rawText.getBytes(); String timestamp = new SimpleDateFormat("yyyy-MM-dd-HHmmss", Locale.US).format(new Date()); String filename = String.format("signal-%s.txt", timestamp); Uri textUri = BlobProvider.getInstance() .forData(textData) .withMimeType(MediaTypes.LONG_TEXT) .withFileName(filename) .createForSingleSessionInMemory(); textSlide = Optional.of(new TextSlide(this, textUri, filename, textData.length)); } return new Pair<>(bodyText, textSlide); } private void markThreadAsRead() { Recipient recipient = this.recipient; new AsyncTask() { @Override protected Void doInBackground(Long... params) { Context context = ConversationActivity.this; List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(params[0], false); if (!SessionMetaProtocol.shouldSendReadReceipt(recipient.getAddress())) { for (MarkedMessageInfo messageInfo : messageIds) { MarkReadReceiver.scheduleDeletion(context, messageInfo.getExpirationInfo()); } } else { MarkReadReceiver.process(context, messageIds); } ApplicationContext.getInstance(context).messageNotifier.updateNotification(context); 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); ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(threadId); } fragment.scrollToBottom(); attachmentManager.cleanup(); updateLinkPreviewState(); } private void sendMessage() { if (inputPanel.isRecordingInLockedMode()) { inputPanel.releaseRecordingLock(); return; } try { Recipient recipient = getRecipient(); if (recipient == null) { throw new RecipientFormattingException("Badly formatted"); } String message = getMessage(); boolean initiating = threadId == -1; boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize; boolean isMediaMessage = attachmentManager.isAttachmentPresent() || recipient.isGroupRecipient() || inputPanel.getQuote().isPresent() || linkPreviewViewModel.hasLinkPreview() || LinkPreviewUtil.isValidMediaUrl(message) || // Loki - Send GIFs as media messages needsSplit; if (isMediaMessage) { sendMediaMessage(initiating); } else { sendTextMessage(initiating); } } catch (RecipientFormattingException ex) { Log.w(TAG, ex); } catch (InvalidMessageException ex) { Log.w(TAG, ex); } if (messageStatus == null && !isGroupConversation() && !(TextSecurePreferences.getLocalNumber(this).equals(recipient.getAddress().serialize()))) { messageStatus = "calculatingPoW"; updateSubtitleTextView(); updateMessageStatusProgressBar(); } } private void sendMediaMessage(boolean initiating) throws InvalidMessageException { Log.i(TAG, "Sending media message..."); sendMediaMessage(getMessage(), attachmentManager.buildSlideDeck(), inputPanel.getQuote().orNull(), linkPreviewViewModel.getActiveLinkPreview(), initiating); } private ListenableFuture sendMediaMessage(String body, SlideDeck slideDeck, QuoteModel quote, Optional linkPreview, final boolean initiating) { Pair> splitMessage = getSplitMessage(body, characterCalculator.calculateCharacters(body).maxPrimaryMessageSize); body = splitMessage.first; if (splitMessage.second.isPresent()) { slideDeck.addSlide(splitMessage.second.get()); } List attachments = slideDeck.asAttachments(); VisibleMessage message = new VisibleMessage(); message.setSentTimestamp(System.currentTimeMillis()); message.setText(body); OutgoingMediaMessage outgoingMessageCandidate = OutgoingMediaMessage.from(message, recipient, attachments, quote, linkPreview.orNull()); final SettableFuture future = new SettableFuture<>(); final Context context = getApplicationContext(); final OutgoingMediaMessage outgoingMessage; outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessageCandidate); ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId); inputPanel.clearQuote(); attachmentManager.clear(glideRequests, false); silentlySetComposeText(""); final long id = fragment.stageOutgoingMessage(outgoingMessage); if (initiating) { DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); } try { long allocatedThreadId = getAllocatedThreadId(context); message.setId(DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMessage, allocatedThreadId, false, ()->fragment.releaseOutgoingMessage(id))); MessageSender.send(message, recipient.getAddress(), attachments, quote, linkPreview.orNull()); sendComplete(allocatedThreadId); } catch (MmsException e) { Log.w(TAG, e); sendComplete(threadId); } future.set(null); return future; } private void sendTextMessage(final boolean initiating) throws InvalidMessageException { final Context context = getApplicationContext(); final String messageBody = getMessage(); VisibleMessage message = new VisibleMessage(); message.setSentTimestamp(System.currentTimeMillis()); message.setText(messageBody); OutgoingTextMessage outgoingTextMessage = OutgoingTextMessage.from(message, recipient); ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId); silentlySetComposeText(""); final long id = fragment.stageOutgoingMessage(outgoingTextMessage); if (initiating) { DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); } long allocatedThreadId = getAllocatedThreadId(context); message.setId(DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(allocatedThreadId, outgoingTextMessage, false, message.getSentTimestamp(), ()->fragment.releaseOutgoingMessage(id))); MessageSender.send(message, recipient.getAddress()); sendComplete(allocatedThreadId); } private void updateToggleButtonState() { if (inputPanel.isRecordingInLockedMode()) { buttonToggle.display(sendButton); quickAttachmentToggle.show(); inlineAttachmentToggle.hide(); return; } if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) { buttonToggle.display(attachButton); quickAttachmentToggle.show(); inlineAttachmentToggle.hide(); } else { buttonToggle.display(sendButton); quickAttachmentToggle.hide(); if (!attachmentManager.isAttachmentPresent() && !linkPreviewViewModel.hasLinkPreview()) { inlineAttachmentToggle.show(); } else { inlineAttachmentToggle.hide(); } } } private void updateLinkPreviewState() { if (TextSecurePreferences.isLinkPreviewsEnabled(this) && !attachmentManager.isAttachmentPresent()) { linkPreviewViewModel.onEnabled(); linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), composeText.getSelectionStart(), composeText.getSelectionEnd()); } else { linkPreviewViewModel.onUserCancel(); } } @Override public void onRecorderPermissionRequired() { Permissions.with(this) .request(Manifest.permission.RECORD_AUDIO) .withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_baseline_mic_48) .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages)) .execute(); } @Override public void onRecorderStarted() { Vibrator vibrator = ServiceUtil.getVibrator(this); vibrator.vibrate(20); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); audioRecorder.startRecording(); audioHandler = new Handler(); stopRecordingTask = () -> inputPanel.onRecordReleased(); audioHandler.postDelayed(stopRecordingTask, 60000); } @Override public void onRecorderLocked() { updateToggleButtonState(); } @Override public void onRecorderFinished() { if (audioHandler != null && stopRecordingTask != null) { audioHandler.removeCallbacks(stopRecordingTask); } updateToggleButtonState(); Vibrator vibrator = ServiceUtil.getVibrator(this); 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) { int subscriptionId = -1; long expiresIn = recipient.getExpireMessages() * 1000L; boolean initiating = threadId == -1; AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first, result.second, MediaTypes.AUDIO_AAC, true); SlideDeck slideDeck = new SlideDeck(); slideDeck.addSlide(audioSlide); sendMediaMessage("", slideDeck, inputPanel.getQuote().orNull(), Optional.absent(), initiating).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Void nothing) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { BlobProvider.getInstance().delete(ConversationActivity.this, result.first); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } }); } @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() { if (audioHandler != null && stopRecordingTask != null) { audioHandler.removeCallbacks(stopRecordingTask); } updateToggleButtonState(); Vibrator vibrator = ServiceUtil.getVibrator(this); 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) { BlobProvider.getInstance().delete(ConversationActivity.this, result.first); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @Override public void onFailure(ExecutionException e) {} }); } @Override public void onEmojiToggle() { if (!emojiDrawerStub.resolved()) { initializeMediaKeyboardProviders(emojiDrawerStub.get()); inputPanel.setMediaKeyboard(emojiDrawerStub.get()); } if (container.getCurrentInput() == emojiDrawerStub.get()) { container.showSoftkey(composeText); } else { container.show(composeText, emojiDrawerStub.get()); } } @Override public void onLinkPreviewCanceled() { linkPreviewViewModel.onUserCancel(); } @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); } } @Override public void onCursorPositionChanged(int start, int end) { linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), start, end); } private void silentlySetComposeText(String text) { typingTextWatcher.setEnabled(false); composeText.setText(text); if (text.isEmpty()) { resetMentions(); } typingTextWatcher.setEnabled(true); } // Listeners private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener { @Override public void onClick(int type) { addAttachment(type); } @Override public void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size) { linkPreviewViewModel.onUserCancel(); Media media = new Media(uri, mimeType, dateTaken, width, height, size, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent()); startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed()), MEDIA_SENDER); } } private class QuickCameraToggleListener implements OnClickListener { @Override public void onClick(View v) { Permissions.with(ConversationActivity.this) .request(Manifest.permission.CAMERA) .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_baseline_photo_camera_48) .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) .onAllGranted(() -> { composeText.clearFocus(); startActivityForResult(MediaSendActivity.buildCameraIntent(ConversationActivity.this, recipient), MEDIA_SENDER); overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary); }) .onAnyDenied(() -> Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show()) .execute(); } } 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) { if (composeText.getTextTrimmed().length() == 0 || beforeLength == 0) { composeText.postDelayed(ConversationActivity.this::updateToggleButtonState, 50); } } @Override public void onTextChanged(CharSequence s, int start, int before,int count) {} @Override public void onFocusChange(View v, boolean hasFocus) {} } private class TypingStatusTextWatcher extends SimpleTextWatcher { private boolean enabled = true; @Override public void onTextChanged(String text) { if (enabled && threadId > 0) { ApplicationContext.getInstance(ConversationActivity.this).getTypingStatusSender().onTypingStarted(threadId); } } public void setEnabled(boolean enabled) { this.enabled = enabled; } } private class MentionTextWatcher extends SimpleTextWatcher { @Override public void onTextChanged(String text) { boolean isBackspace = text.length() < oldText.length(); if (isBackspace) { currentMentionStartIndex = -1; mentionCandidateSelectionView.hide(); mentionCandidateSelectionViewContainer.setVisibility(View.GONE); ArrayList mentionsToRemove = new ArrayList<>(); for (Mention mention : mentions) { if (!text.contains(mention.getDisplayName())) { mentionsToRemove.add(mention); } } mentions.removeAll(mentionsToRemove); } if (text.length() > 0) { if (currentMentionStartIndex > text.length()) { resetMentions(); // Should never occur } int lastCharacterIndex = text.length() - 1; char lastCharacter = text.charAt(lastCharacterIndex); char secondToLastCharacter = ' '; if (lastCharacterIndex > 0) { secondToLastCharacter = text.charAt(lastCharacterIndex - 1); } String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(ConversationActivity.this); LokiThreadDatabase threadDatabase = DatabaseFactory.getLokiThreadDatabase(ConversationActivity.this); LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(ConversationActivity.this); if (lastCharacter == '@' && Character.isWhitespace(secondToLastCharacter)) { List mentionCandidates = MentionsManager.shared.getMentionCandidates("", threadId); currentMentionStartIndex = lastCharacterIndex; mentionCandidateSelectionViewContainer.setVisibility(View.VISIBLE); mentionCandidateSelectionView.show(mentionCandidates, threadId); } else if (Character.isWhitespace(lastCharacter)) { currentMentionStartIndex = -1; mentionCandidateSelectionView.hide(); mentionCandidateSelectionViewContainer.setVisibility(View.GONE); } else { if (currentMentionStartIndex != -1) { String query = text.substring(currentMentionStartIndex + 1); // + 1 to get rid of the @ List mentionCandidates = MentionsManager.shared.getMentionCandidates(query, threadId); mentionCandidateSelectionViewContainer.setVisibility(View.VISIBLE); mentionCandidateSelectionView.show(mentionCandidates, threadId); } } } ConversationActivity.this.oldText = text; } } private void resetMentions() { oldText = ""; currentMentionStartIndex = -1; mentions.clear(); } @Override public void setThreadId(long threadId) { this.threadId = threadId; } @Override public void handleReplyMessage(MessageRecord messageRecord) { if (recipient.isGroupRecipient() && !isActiveGroup()) { return; } Recipient author; if (messageRecord.isOutgoing()) { author = Recipient.from(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)), true); } else { author = messageRecord.getIndividualRecipient(); } if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getSharedContacts().isEmpty()) { Contact contact = ((MmsMessageRecord) messageRecord).getSharedContacts().get(0); String displayName = ContactUtil.getDisplayName(contact); String body = getString(R.string.ConversationActivity_quoted_contact_message, EmojiStrings.BUST_IN_SILHOUETTE, displayName); SlideDeck slideDeck = new SlideDeck(); if (contact.getAvatarAttachment() != null) { slideDeck.addSlide(MediaUtil.getSlideForAttachment(this, contact.getAvatarAttachment())); } inputPanel.setQuote(GlideApp.with(this), messageRecord.getDateSent(), author, body, slideDeck, recipient, threadId); } else if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) { LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0); SlideDeck slideDeck = new SlideDeck(); if (linkPreview.getThumbnail().isPresent()) { slideDeck.addSlide(MediaUtil.getSlideForAttachment(this, linkPreview.getThumbnail().get())); } inputPanel.setQuote(GlideApp.with(this), messageRecord.getDateSent(), author, messageRecord.getBody(), slideDeck, recipient, threadId); } else { inputPanel.setQuote(GlideApp.with(this), messageRecord.getDateSent(), author, messageRecord.getBody(), messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck(), recipient, threadId); } } @Override public void onMessageActionToolbarOpened() { searchViewItem.collapseActionView(); } @Override public void onForwardClicked() { inputPanel.clearQuote(); } @Override public void onAttachmentChanged() { handleSecurityChange(true, isDefaultSms); updateToggleButtonState(); updateLinkPreviewState(); } private class QuoteRestorationTask extends AsyncTask { private final String serialized; private final SettableFuture future; QuoteRestorationTask(@NonNull String serialized, @NonNull SettableFuture future) { this.serialized = serialized; this.future = future; } @Override protected MessageRecord doInBackground(Void... voids) { QuoteId quoteId = QuoteId.deserialize(serialized); if (quoteId != null) { return DatabaseFactory.getMmsSmsDatabase(getApplicationContext()).getMessageFor(quoteId.getId(), quoteId.getAuthor()); } return null; } @Override protected void onPostExecute(MessageRecord messageRecord) { if (messageRecord != null) { handleReplyMessage(messageRecord); future.set(true); } else { Log.e(TAG, "Failed to restore a quote from a draft. No matching message record."); future.set(false); } } } // region Loki private long getAllocatedThreadId(Context context) { long allocatedThreadId; if (threadId == -1) { allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); } else { allocatedThreadId = threadId; } return allocatedThreadId; } private void updateTitleTextView(Recipient recipient) { String userPublicKey = TextSecurePreferences.getLocalNumber(this); if (recipient == null) { titleTextView.setText("Compose"); } else if (recipient.getAddress().toString().toLowerCase().equals(userPublicKey)) { titleTextView.setText(getResources().getString(R.string.note_to_self)); } else { boolean hasName = (recipient.getName() != null && !recipient.getName().isEmpty()); titleTextView.setText(hasName ? recipient.getName() : recipient.getAddress().toString()); } } private void updateProfilePicture() { try { profilePictureView.glide = GlideApp.with(this); profilePictureView.update(recipient, threadId); } catch (Exception exception) { // Do nothing } } private void updateSubtitleTextView() { muteIndicatorImageView.setVisibility(View.GONE); subtitleTextView.setVisibility(View.VISIBLE); if (recipient.isMuted()) { muteIndicatorImageView.setVisibility(View.VISIBLE); subtitleTextView.setText("Muted until " + DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault())); } else if (recipient.isGroupRecipient() && recipient.getName() != null && !recipient.getName().equals("Session Updates") && !recipient.getName().equals("Loki News")) { OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId); if (publicChat != null) { Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(publicChat.getChannel(), publicChat.getServer()); if (userCount == null) { userCount = 0; } subtitleTextView.setText(userCount + " members"); } else if (PublicKeyValidation.isValid(recipient.getAddress().toString())) { subtitleTextView.setText(recipient.getAddress().toString()); } else { subtitleTextView.setVisibility(View.GONE); } } else { subtitleTextView.setVisibility(View.GONE); } titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension((subtitleTextView.getVisibility() == View.GONE) ? R.dimen.very_large_font_size : R.dimen.large_font_size)); } private void setMessageStatusProgressAnimatedIfPossible(int progress) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { messageStatusProgressBar.setProgress(progress, true); } else { messageStatusProgressBar.setProgress(progress); } } private void updateMessageStatusProgressBar() { if (messageStatus != null) { messageStatusProgressBar.setAlpha(1.0f); switch (messageStatus) { case "calculatingPoW": setMessageStatusProgressAnimatedIfPossible(25); break; case "contactingNetwork": setMessageStatusProgressAnimatedIfPossible(50); break; case "sendingMessage": setMessageStatusProgressAnimatedIfPossible(75); break; case "messageSent": setMessageStatusProgressAnimatedIfPossible(100); new Handler().postDelayed(() -> messageStatusProgressBar.animate().alpha(0).setDuration(250).start(), 250); new Handler().postDelayed(() -> messageStatusProgressBar.setProgress(0), 500); break; case "messageFailed": messageStatusProgressBar.animate().alpha(0).setDuration(250).start(); new Handler().postDelayed(() -> messageStatusProgressBar.setProgress(0), 250); break; } } } private void handleMessageStatusChanged(String newMessageStatus, long timestamp) { if (timestamp == 0 || (TextSecurePreferences.getLocalNumber(this).equals(recipient.getAddress().serialize())) ) { return; } updateForNewMessageStatusIfNeeded(newMessageStatus, timestamp); if (newMessageStatus.equals("messageFailed") || newMessageStatus.equals("messageSent")) { new Handler().postDelayed(() -> clearMessageStatusIfNeeded(timestamp), 1000); } } private int precedence(String messageStatus) { if (messageStatus != null) { switch (messageStatus) { case "calculatingPoW": return 0; case "contactingNetwork": return 1; case "sendingMessage": return 2; case "messageSent": return 3; case "messageFailed": return 4; default: return -1; } } else { return -1; } } private void updateForNewMessageStatusIfNeeded(String newMessageStatus, long timestamp) { if (!DatabaseFactory.getSmsDatabase(this).isOutgoingMessage(timestamp) && !DatabaseFactory.getMmsDatabase(this).isOutgoingMessage(timestamp)) { return; } if (precedence(newMessageStatus) > precedence(messageStatus)) { messageStatus = newMessageStatus; updateSubtitleTextView(); updateMessageStatusProgressBar(); } } private void clearMessageStatusIfNeeded(long timestamp) { if (!DatabaseFactory.getSmsDatabase(this).isOutgoingMessage(timestamp) && !DatabaseFactory.getMmsDatabase(this).isOutgoingMessage(timestamp)) { return; } messageStatus = null; updateSubtitleTextView(); updateMessageStatusProgressBar(); } // endregion }