session-android/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java

2436 lines
97 KiB
Java

/*
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.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.signal.OutgoingMediaMessage;
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage;
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.open_groups.OpenGroup;
import org.session.libsession.messaging.open_groups.OpenGroupV2;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.DistributionTypes;
import org.session.libsession.messaging.threads.GroupRecord;
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.session.libsession.utilities.ExpirationUtil;
import org.session.libsession.utilities.GroupUtil;
import org.session.libsession.utilities.MediaTypes;
import org.session.libsession.utilities.ServiceUtil;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsession.utilities.concurrent.AssertedSuccessListener;
import org.session.libsession.utilities.views.Stub;
import org.session.libsignal.libsignal.InvalidMessageException;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.loki.Mention;
import org.session.libsignal.service.loki.utilities.HexEncodingKt;
import org.session.libsignal.service.loki.utilities.PublicKeyValidation;
import org.session.libsignal.utilities.concurrent.ListenableFuture;
import org.session.libsignal.utilities.concurrent.SettableFuture;
import org.session.libsignal.utilities.logging.Log;
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.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.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.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.thoughtcrime.securesms.search.model.MessageResult;
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 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<MediaKeyboard> 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<BroadcastReceiver> broadcastReceivers = new ArrayList<>();
private String messageStatus = null;
// Mentions
private View mentionCandidateSelectionViewContainer;
private MentionCandidateSelectionView mentionCandidateSelectionView;
private int currentMentionStartIndex = -1;
private ArrayList<Mention> 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<Boolean>() {
@Override
public void onSuccess(Boolean result) {
initializeDraft().addListener(new AssertedSuccessListener<Boolean>() {
@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);
OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
if (publicChat != null) {
// Request open group info update and handle the successful result in #onOpenGroupInfoUpdated().
PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel());
} else if (openGroupV2 != null) {
PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom());
}
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<Boolean>() {
@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<Media> 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<Void>() {
@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<Void, Void, Void>() {
@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<Void, Void, Void>() {
@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<Void, Void, Void>() {
@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<Void, Void, Void>() {
@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<Void, Void, Void>() {
@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<Void, Void, IconCompat>() {
@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<Address> 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<Void, Void, Void>() {
@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<Void, Void, Void>() {
@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<Boolean> initializeDraft() {
final SettableFuture<Boolean> result = new SettableFuture<>();
final String draftText = getIntent().getStringExtra(TEXT_EXTRA);
final Uri draftMedia = getIntent().getData();
final MediaType draftMediaType = MediaType.from(getIntent().getType());
final List<Media> 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<Boolean> initializeDraftFromDatabase() {
SettableFuture<Boolean> future = new SettableFuture<>();
new AsyncTask<Void, Void, List<Draft>>() {
@Override
protected List<Draft> doInBackground(Void... params) {
DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this);
List<Draft> results = draftDatabase.getDrafts(threadId);
draftDatabase.clearDrafts(threadId);
return results;
}
@Override
protected void onPostExecute(List<Draft> drafts) {
if (drafts.isEmpty()) {
future.set(false);
updateToggleButtonState();
return;
}
AtomicInteger draftsRemaining = new AtomicInteger(drafts.size());
AtomicBoolean success = new AtomicBoolean(false);
ListenableFuture.Listener<Boolean> listener = new AssertedSuccessListener<Boolean>() {
@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<Boolean> quoteResult = new SettableFuture<>();
new QuoteRestorationTask(draft.getValue(), quoteResult).execute();
quoteResult.addListener(listener);
break;
}
}
updateToggleButtonState();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return future;
}
private ListenableFuture<Boolean> initializeSecurity(final boolean currentSecureText,
final boolean currentIsDefaultSms)
{
final SettableFuture<Boolean> future = new SettableFuture<>();
handleSecurityChange(currentSecureText || isPushGroupConversation(), currentIsDefaultSms);
new AsyncTask<Recipient, Void, boolean[]>() {
@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);
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
if (publicChat != null &&
publicChat.getChannel() == event.getChannel() &&
publicChat.getServer().equals(event.getUrl())) {
this.updateSubtitleTextView();
}
if (openGroup != null &&
openGroup.getRoom().equals(event.getRoom()) &&
openGroup.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<Boolean> setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) {
return setMedia(uri, mediaType, 0, 0);
}
private ListenableFuture<Boolean> 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<QuoteModel> 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<Long> saveDraft() {
final SettableFuture<Long> 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<Long, Void, Long>() {
@Override
protected Long doInBackground(Long... params) {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(ConversationActivity.this);
DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this);
long threadId = params[0];
if (drafts.size() > 0) {
if (threadId == -1) threadId = threadDatabase.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<GroupRecord> 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<String, Optional<Slide>> getSplitMessage(String rawText, int maxPrimaryMessageSize) {
String bodyText = rawText;
Optional<Slide> 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<Long, Void, Void>() {
@Override
protected Void doInBackground(Long... params) {
Context context = ConversationActivity.this;
List<MarkedMessageInfo> 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<Long, Void, Void>() {
@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<Void> sendMediaMessage(String body,
SlideDeck slideDeck,
QuoteModel quote,
Optional<LinkPreview> linkPreview,
final boolean initiating)
{
Pair<String, Optional<Slide>> splitMessage = getSplitMessage(body, characterCalculator.calculateCharacters(body).maxPrimaryMessageSize);
body = splitMessage.first;
if (splitMessage.second.isPresent()) {
slideDeck.addSlide(splitMessage.second.get());
}
List<Attachment> 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<Void> 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<Pair<Uri, Long>> future = audioRecorder.stopRecording();
future.addListener(new ListenableFuture.Listener<Pair<Uri, Long>>() {
@Override
public void onSuccess(final @NonNull Pair<Uri, Long> 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<Void>() {
@Override
public void onSuccess(Void nothing) {
new AsyncTask<Void, Void, Void>() {
@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<Pair<Uri, Long>> future = audioRecorder.stopRecording();
future.addListener(new ListenableFuture.Listener<Pair<Uri, Long>>() {
@Override
public void onSuccess(final Pair<Uri, Long> result) {
new AsyncTask<Void, Void, Void>() {
@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<Mention> 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<Mention> 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<Mention> 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<Void, Void, MessageRecord> {
private final String serialized;
private final SettableFuture<Boolean> future;
QuoteRestorationTask(@NonNull String serialized, @NonNull SettableFuture<Boolean> 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);
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(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 (openGroup != null) {
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(openGroup.getRoom(),openGroup.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
}