diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1d147d9f44..8ee904d263 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -218,6 +218,8 @@
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/longmessage_bubble_received.xml b/res/layout/longmessage_bubble_received.xml
new file mode 100644
index 0000000000..3bfebc6ae6
--- /dev/null
+++ b/res/layout/longmessage_bubble_received.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/longmessage_bubble_sent.xml b/res/layout/longmessage_bubble_sent.xml
new file mode 100644
index 0000000000..a98c7876d6
--- /dev/null
+++ b/res/layout/longmessage_bubble_sent.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index e9534e34e6..836d8a3c9a 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -236,6 +236,7 @@
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3d6ce4f3d9..91f30fcc43 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -133,6 +133,9 @@
Copied %s
from %s
to %s
+ Read More
+ Download More
+ Pending
Reset secure session?
@@ -409,6 +412,11 @@
Failed to send
New safety number
+
+ Unable to find message
+ Message from %1$s
+ Your message
+
Signal
Background connection enabled
diff --git a/res/values/styles.xml b/res/values/styles.xml
index a0ef3aa394..dc23a3887b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -173,6 +173,8 @@
- 2dp
- @null
- 4
+
+
- 2000
- ?conversation_item_sent_text_primary_color
- sentences
diff --git a/src/org/thoughtcrime/securesms/BindableConversationItem.java b/src/org/thoughtcrime/securesms/BindableConversationItem.java
index d92c9704f2..19f5ae207d 100644
--- a/src/org/thoughtcrime/securesms/BindableConversationItem.java
+++ b/src/org/thoughtcrime/securesms/BindableConversationItem.java
@@ -5,6 +5,7 @@ import android.support.annotation.Nullable;
import android.view.View;
import org.thoughtcrime.securesms.contactshare.Contact;
+import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
@@ -34,6 +35,7 @@ public interface BindableConversationItem extends Unbindable {
interface EventListener {
void onQuoteClicked(MmsMessageRecord messageRecord);
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
+ void onMoreTextClicked(@NonNull Address conversationAddress, long messageId, boolean isMms);
void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView);
void onAddToContactsClicked(@NonNull Contact contact);
void onMessageSharedContactClicked(@NonNull List choices);
diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java
index e53a276720..2965a231b4 100644
--- a/src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java
+++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java
@@ -17,17 +17,23 @@ import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
+import org.whispersystems.libsignal.util.guava.Optional;
public class EmojiTextView extends AppCompatTextView {
private final boolean scaleEmojis;
+ private static final char ELLIPSIS = '…';
+
private CharSequence previousText;
private BufferType previousBufferType;
private float originalFontSize;
private boolean useSystemEmoji;
private boolean sizeChangeInProgress;
+ private int maxLength;
+ private CharSequence overflowText;
+ private CharSequence previousOverflowText;
public EmojiTextView(Context context) {
this(context, null);
@@ -42,6 +48,7 @@ public class EmojiTextView extends AppCompatTextView {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
+ maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
a.recycle();
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
@@ -67,21 +74,22 @@ public class EmojiTextView extends AppCompatTextView {
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalFontSize);
}
- if (unchanged(text, type)) {
+ if (unchanged(text, overflowText, type)) {
return;
}
- previousText = text;
- previousBufferType = type;
- useSystemEmoji = useSystemEmoji();
+ previousText = text;
+ previousOverflowText = overflowText;
+ previousBufferType = type;
+ useSystemEmoji = useSystemEmoji();
- if (useSystemEmoji || candidates == null || candidates.size() == 0) {
- super.setText(text, BufferType.NORMAL);
+ if (maxLength <= 0 && (useSystemEmoji || candidates == null || candidates.size() == 0)) {
+ super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")).append(Optional.fromNullable(overflowText).or("")), BufferType.NORMAL);
return;
}
CharSequence emojified = provider.emojify(candidates, text, this);
- super.setText(emojified, BufferType.SPANNABLE);
+ super.setText(new SpannableStringBuilder(emojified).append(Optional.fromNullable(overflowText).or("")), BufferType.SPANNABLE);
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
// We ellipsize them ourselves by manually truncating the appropriate section.
@@ -90,7 +98,23 @@ public class EmojiTextView extends AppCompatTextView {
}
}
+ public void setOverflowText(@Nullable CharSequence overflowText) {
+ this.overflowText = overflowText;
+ setText(previousText, BufferType.SPANNABLE);
+ }
+
private void ellipsize() {
+ if (maxLength > 0 && getText().length() > maxLength + 1) {
+ SpannableStringBuilder newContent = new SpannableStringBuilder();
+ newContent.append(getText().subSequence(0, maxLength)).append(ELLIPSIS).append(Optional.fromNullable(overflowText).or(""));
+
+ EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent);
+ CharSequence emojified = EmojiProvider.getInstance(getContext()).emojify(newCandidates, newContent, this);
+
+ super.setText(emojified, BufferType.SPANNABLE);
+ return;
+ }
+
post(() -> {
if (getLayout() == null) {
ellipsize();
@@ -98,10 +122,10 @@ public class EmojiTextView extends AppCompatTextView {
}
int maxLines = TextViewCompat.getMaxLines(EmojiTextView.this);
- if (maxLines <= 0) {
+ if (maxLines <= 0 && maxLength < 0) {
return;
}
-
+
int lineCount = getLineCount();
if (lineCount > maxLines) {
int overflowStart = getLayout().getLineStart(maxLines - 1);
@@ -110,7 +134,8 @@ public class EmojiTextView extends AppCompatTextView {
SpannableStringBuilder newContent = new SpannableStringBuilder();
newContent.append(getText().subSequence(0, overflowStart))
- .append(ellipsized.subSequence(0, ellipsized.length()));
+ .append(ellipsized.subSequence(0, ellipsized.length()))
+ .append(Optional.fromNullable(overflowText).or(""));
EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent);
CharSequence emojified = EmojiProvider.getInstance(getContext()).emojify(newCandidates, newContent, this);
@@ -120,10 +145,11 @@ public class EmojiTextView extends AppCompatTextView {
});
}
- private boolean unchanged(CharSequence text, BufferType bufferType) {
- return Util.equals(previousText, text) &&
- Util.equals(previousBufferType, bufferType) &&
- useSystemEmoji == useSystemEmoji() &&
+ private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) {
+ return Util.equals(previousText, text) &&
+ Util.equals(previousOverflowText, overflowText) &&
+ Util.equals(previousBufferType, bufferType) &&
+ useSystemEmoji == useSystemEmoji() &&
!sizeChangeInProgress;
}
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index c7be9d0211..e23679e838 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -170,12 +170,14 @@ import org.thoughtcrime.securesms.mms.QuoteId;
import org.thoughtcrime.securesms.mms.QuoteModel;
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.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
+import org.thoughtcrime.securesms.providers.MemoryBlobProvider;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
@@ -212,9 +214,12 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
+import java.text.SimpleDateFormat;
import java.util.Collections;
+import java.util.Date;
import java.util.LinkedList;
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;
@@ -1905,7 +1910,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
charactersLeft.setText(String.format(dynamicLanguage.getCurrentLocale(),
"%d/%d (%d)",
characterState.charactersRemaining,
- characterState.maxMessageSize,
+ characterState.maxTotalMessageSize,
characterState.messagesSpent));
charactersLeft.setVisibility(View.VISIBLE);
} else {
@@ -1961,6 +1966,24 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return rawText;
}
+ private Pair> getSplitMessage(String rawText, int maxPrimaryMessageSize) {
+ String bodyText = rawText;
+ Optional extraText = Optional.absent();
+
+ if (bodyText.length() > maxPrimaryMessageSize) {
+ bodyText = rawText.substring(0, maxPrimaryMessageSize);
+
+ byte[] extraData = rawText.substring(maxPrimaryMessageSize).getBytes();
+ Uri textUri = MemoryBlobProvider.getInstance().createUri(extraData);
+ String timestamp = new SimpleDateFormat("yyyy-MM-dd-HHmmss", Locale.US).format(new Date());
+ String filename = String.format("signal-%s.txt", timestamp);
+
+ extraText = Optional.of(new TextSlide(this, textUri, filename, extraData.length));
+ }
+
+ return new Pair<>(bodyText, extraText);
+ }
+
private MediaConstraints getCurrentMediaConstraints() {
return sendButton.getSelectedTransport().getType() == Type.TEXTSECURE
? MediaConstraints.getPushMediaConstraints()
@@ -2021,6 +2044,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
throw new RecipientFormattingException("Badly formatted");
}
+ String message = getMessage();
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
long expiresIn = recipient.getExpireMessages() * 1000L;
@@ -2029,7 +2053,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
recipient.isGroupRecipient() ||
recipient.getAddress().isEmail() ||
inputPanel.getQuote().isPresent() ||
- linkPreviewViewModel.hasLinkPreview();
+ linkPreviewViewModel.hasLinkPreview() ||
+ message.length() > sendButton.getSelectedTransport().calculateCharacters(message).maxPrimaryMessageSize;
Log.i(TAG, "isManual Selection: " + sendButton.isManualSelection());
Log.i(TAG, "forceSms: " + forceSms);
@@ -2078,6 +2103,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return new SettableFuture<>(null);
}
+ if (isSecureText && !forceSms) {
+ Pair> splitMessage = getSplitMessage(body, sendButton.getSelectedTransport().calculateCharacters(body).maxPrimaryMessageSize);
+ body = splitMessage.first;
+
+ if (splitMessage.second.isPresent()) {
+ slideDeck.addSlide(splitMessage.second.get());
+ }
+ }
+
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType, inputPanel.getQuote().orNull(), contacts, previews);
final SettableFuture future = new SettableFuture<>();
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
index 43a7fbc76d..1dceaa181c 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -82,9 +82,11 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
+import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
+import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.profiles.UnknownSenderView;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -100,6 +102,8 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional;
+import java.io.IOException;
+import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -516,43 +520,60 @@ public class ConversationFragment extends Fragment
}
private void handleForwardMessage(MessageRecord message) {
- Intent composeIntent = new Intent(getActivity(), ShareActivity.class);
- composeIntent.putExtra(Intent.EXTRA_TEXT, message.getDisplayBody().toString());
- if (message.isMms()) {
- MmsMessageRecord mediaMessage = (MmsMessageRecord) message;
- boolean isAlbum = mediaMessage.containsMediaSlide() &&
- mediaMessage.getSlideDeck().getSlides().size() > 1 &&
- mediaMessage.getSlideDeck().getAudioSlide() == null &&
- mediaMessage.getSlideDeck().getDocumentSlide() == null;
+ SimpleTask.run(getLifecycle(), () -> {
+ Intent composeIntent = new Intent(getActivity(), ShareActivity.class);
+ composeIntent.putExtra(Intent.EXTRA_TEXT, message.getDisplayBody().toString());
- if (isAlbum) {
- ArrayList mediaList = new ArrayList<>(mediaMessage.getSlideDeck().getSlides().size());
+ if (message.isMms()) {
+ MmsMessageRecord mediaMessage = (MmsMessageRecord) message;
+ boolean isAlbum = mediaMessage.containsMediaSlide() &&
+ mediaMessage.getSlideDeck().getSlides().size() > 1 &&
+ mediaMessage.getSlideDeck().getAudioSlide() == null &&
+ mediaMessage.getSlideDeck().getDocumentSlide() == null;
- for (Attachment attachment : mediaMessage.getSlideDeck().asAttachments()) {
- Uri uri = attachment.getDataUri() != null ? attachment.getDataUri() : attachment.getThumbnailUri();
+ if (isAlbum) {
+ ArrayList mediaList = new ArrayList<>(mediaMessage.getSlideDeck().getSlides().size());
+ List attachments = Stream.of(mediaMessage.getSlideDeck().getSlides())
+ .filter(s -> s.hasImage() || s.hasVideo())
+ .map(Slide::asAttachment)
+ .toList();
- if (uri != null) {
- mediaList.add(new Media(uri,
- attachment.getContentType(),
- System.currentTimeMillis(),
- attachment.getWidth(),
- attachment.getHeight(),
- attachment.getSize(),
- Optional.absent(),
- Optional.fromNullable(attachment.getCaption())));
+ for (Attachment attachment : attachments) {
+ Uri uri = attachment.getDataUri() != null ? attachment.getDataUri() : attachment.getThumbnailUri();
+
+ if (uri != null) {
+ mediaList.add(new Media(uri,
+ attachment.getContentType(),
+ System.currentTimeMillis(),
+ attachment.getWidth(),
+ attachment.getHeight(),
+ attachment.getSize(),
+ Optional.absent(),
+ Optional.fromNullable(attachment.getCaption())));
+ }
+ };
+
+ if (!mediaList.isEmpty()) {
+ composeIntent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaList);
+ }
+ } else if (mediaMessage.containsMediaSlide()) {
+ Slide slide = mediaMessage.getSlideDeck().getSlides().get(0);
+ composeIntent.putExtra(Intent.EXTRA_STREAM, slide.getUri());
+ composeIntent.setType(slide.getContentType());
+ }
+
+ if (mediaMessage.getSlideDeck().getTextSlide() != null && mediaMessage.getSlideDeck().getTextSlide().getUri() != null) {
+ try (InputStream stream = PartAuthority.getAttachmentStream(requireContext(), mediaMessage.getSlideDeck().getTextSlide().getUri())) {
+ String extraText = Util.readFullyAsString(stream);
+ composeIntent.putExtra(Intent.EXTRA_TEXT, message.getDisplayBody().toString() + extraText);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to read long message text when forwarding.");
}
}
-
- if (!mediaList.isEmpty()) {
- composeIntent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaList);
- }
- } else if (mediaMessage.containsMediaSlide()) {
- Slide slide = mediaMessage.getSlideDeck().getSlides().get(0);
- composeIntent.putExtra(Intent.EXTRA_STREAM, slide.getUri());
- composeIntent.setType(slide.getContentType());
}
- }
- startActivity(composeIntent);
+
+ return composeIntent;
+ }, this::startActivity);
}
private void handleResendMessage(final MessageRecord message) {
@@ -910,6 +931,13 @@ public class ConversationFragment extends Fragment
}
}
+ @Override
+ public void onMoreTextClicked(@NonNull Address conversationAddress, long messageId, boolean isMms) {
+ if (getContext() != null && getActivity() != null) {
+ startActivity(LongMessageActivity.getIntent(getContext(), conversationAddress, messageId, isMms));
+ }
+ }
+
@Override
public void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView) {
if (getContext() != null && getActivity() != null) {
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java
index e83ce53770..53b4805c47 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java
@@ -23,6 +23,7 @@ import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
+import android.graphics.Typeface;
import android.net.Uri;
import android.support.annotation.DimenRes;
import android.support.annotation.NonNull;
@@ -30,9 +31,13 @@ import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.text.Spannable;
import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
import android.text.Spanned;
+import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
+import android.text.style.CharacterStyle;
+import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.URLSpan;
import android.text.util.Linkify;
@@ -46,6 +51,8 @@ import org.thoughtcrime.securesms.MessageDetailsActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.LinkPreviewView;
+import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
+import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
@@ -87,6 +94,7 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
+import org.thoughtcrime.securesms.mms.TextSlide;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.util.DateUtils;
@@ -119,7 +127,8 @@ public class ConversationItem extends LinearLayout
{
private static final String TAG = ConversationItem.class.getSimpleName();
- private static final int MAX_MEASURE_CALLS = 3;
+ private static final int MAX_MEASURE_CALLS = 3;
+ private static final int MAX_BODY_DISPLAY_LENGTH = 1000;
private MessageRecord messageRecord;
private Locale locale;
@@ -129,7 +138,7 @@ public class ConversationItem extends LinearLayout
protected ViewGroup bodyBubble;
private QuoteView quoteView;
- private TextView bodyText;
+ private EmojiTextView bodyText;
private ConversationItemFooter footer;
private TextView groupSender;
private TextView groupSenderProfileName;
@@ -378,7 +387,7 @@ public class ConversationItem extends LinearLayout
}
private boolean isCaptionlessMms(MessageRecord messageRecord) {
- return TextUtils.isEmpty(messageRecord.getDisplayBody()) && messageRecord.isMms();
+ return TextUtils.isEmpty(messageRecord.getDisplayBody()) && messageRecord.isMms() && ((MmsMessageRecord) messageRecord).getSlideDeck().getTextSlide() == null;
}
private boolean hasAudio(MessageRecord messageRecord) {
@@ -397,6 +406,13 @@ public class ConversationItem extends LinearLayout
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null;
}
+ private boolean hasExtraText(MessageRecord messageRecord) {
+ boolean hasTextSlide = messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getTextSlide() != null;
+ boolean hasOverflowText = messageRecord.getBody().length() > MAX_BODY_DISPLAY_LENGTH;
+
+ return hasTextSlide || hasOverflowText;
+ }
+
private boolean hasQuote(MessageRecord messageRecord) {
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getQuote() != null;
}
@@ -421,6 +437,12 @@ public class ConversationItem extends LinearLayout
styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery);
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery);
+ if (hasExtraText(messageRecord)) {
+ bodyText.setOverflowText(getLongMessageSpan(messageRecord));
+ } else {
+ bodyText.setOverflowText(null);
+ }
+
bodyText.setText(styledText);
bodyText.setVisibility(View.VISIBLE);
}
@@ -536,7 +558,7 @@ public class ConversationItem extends LinearLayout
mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener);
mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener);
mediaThumbnailStub.get().setOnClickListener(passthroughClickListener);
- mediaThumbnailStub.get().showShade(TextUtils.isEmpty(messageRecord.getDisplayBody()));
+ mediaThumbnailStub.get().showShade(TextUtils.isEmpty(messageRecord.getDisplayBody()) && !hasExtraText(messageRecord));
mediaThumbnailStub.get().setConversationColor(messageRecord.isOutgoing() ? defaultBubbleColor
: messageRecord.getRecipient().getColor().toConversationColor(context));
@@ -613,7 +635,7 @@ public class ConversationItem extends LinearLayout
topRight = 0;
}
- if (hasLinkPreview(messageRecord)) {
+ if (hasLinkPreview(messageRecord) || hasExtraText(messageRecord)) {
bottomLeft = 0;
bottomRight = 0;
}
@@ -748,7 +770,7 @@ public class ConversationItem extends LinearLayout
ViewUtil.updateLayoutParams(footer, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
footer.setVisibility(GONE);
- if (sharedContactStub.resolved()) sharedContactStub.get().getFooter().setVisibility(GONE);
+ if (sharedContactStub.resolved()) sharedContactStub.get().getFooter().setVisibility(GONE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().getFooter().setVisibility(GONE);
boolean differentTimestamps = next.isPresent() && !DateUtils.isSameExtendedRelativeTimestamp(context, locale, next.get().getTimestamp(), current.getTimestamp());
@@ -899,6 +921,49 @@ public class ConversationItem extends LinearLayout
new ConfirmIdentityDialog(context, messageRecord, mismatches.get(0)).show();
}
+ private Spannable getLongMessageSpan(@NonNull MessageRecord messageRecord) {
+ String message;
+ Runnable action;
+
+ if (messageRecord.isMms()) {
+ TextSlide slide = ((MmsMessageRecord) messageRecord).getSlideDeck().getTextSlide();
+
+ if (slide != null && slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
+ message = getResources().getString(R.string.ConversationItem_read_more);
+ action = () -> eventListener.onMoreTextClicked(conversationRecipient.getAddress(), messageRecord.getId(), messageRecord.isMms());
+ } else if (slide != null && slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
+ message = getResources().getString(R.string.ConversationItem_pending);
+ action = () -> {};
+ } else if (slide != null) {
+ message = getResources().getString(R.string.ConversationItem_download_more);
+ action = () -> singleDownloadClickListener.onClick(bodyText, slide);
+ } else {
+ message = getResources().getString(R.string.ConversationItem_read_more);
+ action = () -> eventListener.onMoreTextClicked(conversationRecipient.getAddress(), messageRecord.getId(), messageRecord.isMms());
+ }
+ } else {
+ message = getResources().getString(R.string.ConversationItem_read_more);
+ action = () -> eventListener.onMoreTextClicked(conversationRecipient.getAddress(), messageRecord.getId(), messageRecord.isMms());
+ }
+
+ SpannableStringBuilder span = new SpannableStringBuilder(message);
+ CharacterStyle style = new ClickableSpan() {
+ @Override
+ public void onClick(@NonNull View widget) {
+ if (eventListener != null && batchSelected.isEmpty()) {
+ action.run();
+ }
+ }
+
+ @Override
+ public void updateDrawState(@NonNull TextPaint ds) {
+ ds.setTypeface(Typeface.DEFAULT_BOLD);
+ }
+ };
+ span.setSpan(style, 0, span.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ return span;
+ }
+
@Override
public void onModified(final Recipient modified) {
Util.runOnMain(() -> {
diff --git a/src/org/thoughtcrime/securesms/database/MediaDatabase.java b/src/org/thoughtcrime/securesms/database/MediaDatabase.java
index 2f3fb2ca39..3f837a6224 100644
--- a/src/org/thoughtcrime/securesms/database/MediaDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MediaDatabase.java
@@ -51,7 +51,10 @@ public class MediaDatabase extends Database {
+ "ORDER BY " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " DESC";
private static final String GALLERY_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " LIKE 'image/%' OR " + AttachmentDatabase.CONTENT_TYPE + " LIKE 'video/%'");
- private static final String DOCUMENT_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'image/%' AND " + AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'video/%' AND " + AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'audio/%'");
+ private static final String DOCUMENT_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'image/%' AND " +
+ AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'video/%' AND " +
+ AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'audio/%' AND " +
+ AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'text/x-signal-plain'");
MediaDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
index 1a1e146368..15b2c025d1 100644
--- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
@@ -43,8 +43,6 @@ import java.util.List;
*/
public abstract class MessageRecord extends DisplayRecord {
- private static final int MAX_DISPLAY_LENGTH = 2000;
-
private final Recipient individualRecipient;
private final int recipientDeviceId;
private final long id;
@@ -123,8 +121,6 @@ public abstract class MessageRecord extends DisplayRecord {
} else if (isIdentityDefault()) {
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().toShortString()));
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, getIndividualRecipient().toShortString()));
- } else if (getBody().length() > MAX_DISPLAY_LENGTH) {
- return new SpannableString(getBody().substring(0, MAX_DISPLAY_LENGTH));
}
return new SpannableString(getBody());
diff --git a/src/org/thoughtcrime/securesms/longmessage/LongMessage.java b/src/org/thoughtcrime/securesms/longmessage/LongMessage.java
new file mode 100644
index 0000000000..4e2c8c1219
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/longmessage/LongMessage.java
@@ -0,0 +1,26 @@
+package org.thoughtcrime.securesms.longmessage;
+
+import org.thoughtcrime.securesms.database.model.MessageRecord;
+
+/**
+ * A wrapper around a {@link MessageRecord} and its extra text attachment expanded into a string
+ * held in memory.
+ */
+class LongMessage {
+
+ private final MessageRecord messageRecord;
+ private final String extraBody;
+
+ LongMessage(MessageRecord messageRecord, String extraBody) {
+ this.messageRecord = messageRecord;
+ this.extraBody = extraBody;
+ }
+
+ MessageRecord getMessageRecord() {
+ return messageRecord;
+ }
+
+ String getFullBody() {
+ return messageRecord.getBody() + extraBody;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java b/src/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java
new file mode 100644
index 0000000000..e7efcaa081
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java
@@ -0,0 +1,149 @@
+package org.thoughtcrime.securesms.longmessage;
+
+import android.arch.lifecycle.ViewModelProviders;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.color.MaterialColor;
+import org.thoughtcrime.securesms.components.ConversationItemFooter;
+import org.thoughtcrime.securesms.database.Address;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
+import org.thoughtcrime.securesms.util.DynamicLanguage;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+import org.thoughtcrime.securesms.util.ThemeUtil;
+import org.thoughtcrime.securesms.util.Util;
+import org.thoughtcrime.securesms.util.views.Stub;
+
+public class LongMessageActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener {
+
+ private static final String KEY_ADDRESS = "address";
+ private static final String KEY_MESSAGE_ID = "message_id";
+ private static final String KEY_IS_MMS = "is_mms";
+
+ private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
+ private final DynamicTheme dynamicTheme = new DynamicTheme();
+
+ private Stub sentBubble;
+ private Stub receivedBubble;
+
+ private LongMessageViewModel viewModel;
+
+ public static Intent getIntent(@NonNull Context context, @NonNull Address conversationAddress, long messageId, boolean isMms) {
+ Intent intent = new Intent(context, LongMessageActivity.class);
+ intent.putExtra(KEY_ADDRESS, conversationAddress.serialize());
+ intent.putExtra(KEY_MESSAGE_ID, messageId);
+ intent.putExtra(KEY_IS_MMS, isMms);
+ return intent;
+ }
+
+ @Override
+ protected void onPreCreate() {
+ super.onPreCreate();
+ dynamicLanguage.onCreate(this);
+ dynamicTheme.onCreate(this);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState, boolean ready) {
+ super.onCreate(savedInstanceState, ready);
+ setContentView(R.layout.longmessage_activity);
+
+ sentBubble = new Stub<>(findViewById(R.id.longmessage_sent_stub));
+ receivedBubble = new Stub<>(findViewById(R.id.longmessage_received_stub));
+
+ initViewModel(getIntent().getLongExtra(KEY_MESSAGE_ID, -1), getIntent().getBooleanExtra(KEY_IS_MMS, false));
+
+ Recipient conversationRecipient = Recipient.from(this, Address.fromSerialized(getIntent().getStringExtra(KEY_ADDRESS)), true);
+ conversationRecipient.addListener(this);
+ updateActionBarColor(conversationRecipient.getColor());
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ dynamicLanguage.onResume(this);
+ dynamicTheme.onResume(this);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ super.onOptionsItemSelected(item);
+
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onModified(final Recipient recipient) {
+ Util.runOnMain(() -> updateActionBarColor(recipient.getColor()));
+ }
+
+ private void updateActionBarColor(@NonNull MaterialColor color) {
+ getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ getWindow().setStatusBarColor(color.toStatusBarColor(this));
+ }
+ }
+
+ private void initViewModel(long messageId, boolean isMms) {
+ viewModel = ViewModelProviders.of(this, new LongMessageViewModel.Factory(getApplication(), new LongMessageRepository(this), messageId, isMms))
+ .get(LongMessageViewModel.class);
+
+ viewModel.getMessage().observe(this, message -> {
+ if (message == null) return;
+
+ if (!message.isPresent()) {
+ Toast.makeText(this, R.string.LongMessageActivity_unable_to_find_message, Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+
+
+ if (message.get().getMessageRecord().isOutgoing()) {
+ getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_your_message));
+ } else {
+ Recipient recipient = message.get().getMessageRecord().getRecipient();
+ String name = Util.getFirstNonEmpty(recipient.getName(), recipient.getProfileName(), recipient.getAddress().serialize()) ;
+ getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_message_from_s, name));
+ }
+
+ ViewGroup bubble;
+
+ if (message.get().getMessageRecord().isOutgoing()) {
+ bubble = sentBubble.get();
+ bubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.conversation_item_bubble_background), PorterDuff.Mode.MULTIPLY);
+ } else {
+ bubble = receivedBubble.get();
+ bubble.getBackground().setColorFilter(message.get().getMessageRecord().getRecipient().getColor().toConversationColor(this), PorterDuff.Mode.MULTIPLY);
+ }
+
+ TextView text = bubble.findViewById(R.id.longmessage_text);
+ ConversationItemFooter footer = bubble.findViewById(R.id.longmessage_footer);
+
+ bubble.setVisibility(View.VISIBLE);
+ text.setText(message.get().getFullBody());
+ footer.setMessageRecord(message.get().getMessageRecord(), dynamicLanguage.getCurrentLocale());
+ });
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/longmessage/LongMessageRepository.java b/src/org/thoughtcrime/securesms/longmessage/LongMessageRepository.java
new file mode 100644
index 0000000000..a39faaf795
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/longmessage/LongMessageRepository.java
@@ -0,0 +1,103 @@
+package org.thoughtcrime.securesms.longmessage;
+
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.MutableLiveData;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.MmsDatabase;
+import org.thoughtcrime.securesms.database.SmsDatabase;
+import org.thoughtcrime.securesms.database.model.MessageRecord;
+import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.mms.PartAuthority;
+import org.thoughtcrime.securesms.mms.TextSlide;
+import org.thoughtcrime.securesms.util.Util;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+class LongMessageRepository {
+
+ private final static String TAG = LongMessageRepository.class.getSimpleName();
+
+ private final MmsDatabase mmsDatabase;
+ private final SmsDatabase smsDatabase;
+
+ LongMessageRepository(@NonNull Context context) {
+ this.mmsDatabase = DatabaseFactory.getMmsDatabase(context);
+ this.smsDatabase = DatabaseFactory.getSmsDatabase(context);
+ }
+
+ void getMessage(@NonNull Context context, long messageId, boolean isMms, @NonNull Callback> callback) {
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
+ if (isMms) {
+ callback.onComplete(getMmsLongMessage(context, mmsDatabase, messageId));
+ } else {
+ callback.onComplete(getSmsLongMessage(smsDatabase, messageId));
+ }
+ });
+ }
+
+ @WorkerThread
+ private Optional getMmsLongMessage(@NonNull Context context, @NonNull MmsDatabase mmsDatabase, long messageId) {
+ Optional record = getMmsMessage(mmsDatabase, messageId);
+
+ if (record.isPresent()) {
+ TextSlide textSlide = record.get().getSlideDeck().getTextSlide();
+
+ if (textSlide != null && textSlide.getUri() != null) {
+ return Optional.of(new LongMessage(record.get(), readFullBody(context, textSlide.getUri())));
+ } else {
+ return Optional.of(new LongMessage(record.get(), ""));
+ }
+ } else {
+ return Optional.absent();
+ }
+ }
+
+ @WorkerThread
+ private Optional getSmsLongMessage(@NonNull SmsDatabase smsDatabase, long messageId) {
+ Optional record = getSmsMessage(smsDatabase, messageId);
+
+ if (record.isPresent()) {
+ return Optional.of(new LongMessage(record.get(), ""));
+ } else {
+ return Optional.absent();
+ }
+ }
+
+
+ @WorkerThread
+ private Optional getMmsMessage(@NonNull MmsDatabase mmsDatabase, long messageId) {
+ try (Cursor cursor = mmsDatabase.getMessage(messageId)) {
+ return Optional.fromNullable((MmsMessageRecord) mmsDatabase.readerFor(cursor).getNext());
+ }
+ }
+
+ @WorkerThread
+ private Optional getSmsMessage(@NonNull SmsDatabase smsDatabase, long messageId) {
+ try (Cursor cursor = smsDatabase.getMessageCursor(messageId)) {
+ return Optional.fromNullable(smsDatabase.readerFor(cursor).getNext());
+ }
+ }
+
+ private String readFullBody(@NonNull Context context, @NonNull Uri uri) {
+ try (InputStream stream = PartAuthority.getAttachmentStream(context, uri)) {
+ return Util.readFullyAsString(stream);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to read full text body.", e);
+ return "";
+ }
+ }
+
+ interface Callback {
+ void onComplete(T result);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/longmessage/LongMessageViewModel.java b/src/org/thoughtcrime/securesms/longmessage/LongMessageViewModel.java
new file mode 100644
index 0000000000..681874c0e4
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/longmessage/LongMessageViewModel.java
@@ -0,0 +1,80 @@
+package org.thoughtcrime.securesms.longmessage;
+
+import android.app.Application;
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.MutableLiveData;
+import android.arch.lifecycle.ViewModel;
+import android.arch.lifecycle.ViewModelProvider;
+
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+
+import org.thoughtcrime.securesms.database.DatabaseContentProviders;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+class LongMessageViewModel extends ViewModel {
+
+ private final Application application;
+ private final LongMessageRepository repository;
+ private final long messageId;
+ private final boolean isMms;
+
+ private final MutableLiveData> message;
+ private final MessageObserver messageObserver;
+
+ private LongMessageViewModel(@NonNull Application application, @NonNull LongMessageRepository repository, long messageId, boolean isMms) {
+ this.application = application;
+ this.repository = repository;
+ this.messageId = messageId;
+ this.isMms = isMms;
+ this.message = new MutableLiveData<>();
+ this.messageObserver = new MessageObserver(new Handler());
+ }
+
+ LiveData> getMessage() {
+ repository.getMessage(application, messageId, isMms, longMessage -> {
+ if (longMessage.isPresent()) {
+ application.getContentResolver().registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(longMessage.get().getMessageRecord().getThreadId()), true, messageObserver);
+ }
+ message.postValue(longMessage);
+ });
+ return message;
+ }
+
+ @Override
+ protected void onCleared() {
+ application.getContentResolver().unregisterContentObserver(messageObserver);
+ }
+
+ private class MessageObserver extends ContentObserver {
+ MessageObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ getMessage();
+ }
+ }
+
+ static class Factory extends ViewModelProvider.NewInstanceFactory {
+
+ private final Application context;
+ private final LongMessageRepository repository;
+ private final long messageId;
+ private final boolean isMms;
+
+ public Factory(@NonNull Application application, @NonNull LongMessageRepository repository, long messageId, boolean isMms) {
+ this.context = application;
+ this.repository = repository;
+ this.messageId = messageId;
+ this.isMms = isMms;
+ }
+
+ @Override
+ public @NonNull T create(@NonNull Class modelClass) {
+ return modelClass.cast(new LongMessageViewModel(context, repository, messageId, isMms));
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
index 34b1718e08..eff3c50d71 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.mediasend;
import android.annotation.SuppressLint;
-import android.app.ProgressDialog;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
import android.graphics.Bitmap;
@@ -358,7 +357,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
charactersLeft.setText(String.format(locale,
"%d/%d (%d)",
characterState.charactersRemaining,
- characterState.maxMessageSize,
+ characterState.maxTotalMessageSize,
characterState.messagesSpent));
charactersLeft.setVisibility(View.VISIBLE);
} else {
diff --git a/src/org/thoughtcrime/securesms/mms/SlideDeck.java b/src/org/thoughtcrime/securesms/mms/SlideDeck.java
index d81524a194..caf09bdffc 100644
--- a/src/org/thoughtcrime/securesms/mms/SlideDeck.java
+++ b/src/org/thoughtcrime/securesms/mms/SlideDeck.java
@@ -128,4 +128,14 @@ public class SlideDeck {
return null;
}
+
+ public @Nullable TextSlide getTextSlide() {
+ for (Slide slide: slides) {
+ if (MediaUtil.isLongTextType(slide.getContentType())) {
+ return (TextSlide)slide;
+ }
+ }
+
+ return null;
+ }
}
diff --git a/src/org/thoughtcrime/securesms/mms/TextSlide.java b/src/org/thoughtcrime/securesms/mms/TextSlide.java
new file mode 100644
index 0000000000..e4c20d0619
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/mms/TextSlide.java
@@ -0,0 +1,22 @@
+package org.thoughtcrime.securesms.mms;
+
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.thoughtcrime.securesms.attachments.Attachment;
+import org.thoughtcrime.securesms.util.MediaUtil;
+import org.thoughtcrime.securesms.util.StorageUtil;
+
+public class TextSlide extends Slide {
+
+ public TextSlide(@NonNull Context context, @NonNull Attachment attachment) {
+ super(context, attachment);
+ }
+
+ public TextSlide(@NonNull Context context, @NonNull Uri uri, @Nullable String filename, long size) {
+ super(context, constructAttachmentFromUri(context, uri, MediaUtil.LONG_TEXT, size, 0, 0, true, filename, null, false, false));
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/scribbles/ScribbleHud.java b/src/org/thoughtcrime/securesms/scribbles/ScribbleHud.java
index 03955fc746..e16d33b063 100644
--- a/src/org/thoughtcrime/securesms/scribbles/ScribbleHud.java
+++ b/src/org/thoughtcrime/securesms/scribbles/ScribbleHud.java
@@ -344,7 +344,7 @@ public class ScribbleHud extends InputAwareLayout implements ViewTreeObserver.On
charactersLeft.setText(String.format(locale,
"%d/%d (%d)",
characterState.charactersRemaining,
- characterState.maxMessageSize,
+ characterState.maxTotalMessageSize,
characterState.messagesSpent));
charactersLeft.setVisibility(View.VISIBLE);
} else {
diff --git a/src/org/thoughtcrime/securesms/util/AttachmentUtil.java b/src/org/thoughtcrime/securesms/util/AttachmentUtil.java
index 61d43ebbc5..8fbb687837 100644
--- a/src/org/thoughtcrime/securesms/util/AttachmentUtil.java
+++ b/src/org/thoughtcrime/securesms/util/AttachmentUtil.java
@@ -37,7 +37,7 @@ public class AttachmentUtil {
Set allowedTypes = getAllowedAutoDownloadTypes(context);
String contentType = attachment.getContentType();
- if (attachment.isVoiceNote() || (MediaUtil.isAudio(attachment) && TextUtils.isEmpty(attachment.getFileName()))) {
+ if (attachment.isVoiceNote() || (MediaUtil.isAudio(attachment) && TextUtils.isEmpty(attachment.getFileName())) || MediaUtil.isLongTextType(attachment.getContentType())) {
return true;
} else if (isNonDocumentType(contentType)) {
return allowedTypes.contains(MediaUtil.getDiscreteMimeType(contentType));
diff --git a/src/org/thoughtcrime/securesms/util/CharacterCalculator.java b/src/org/thoughtcrime/securesms/util/CharacterCalculator.java
index f9b95f1fea..c499c83d78 100644
--- a/src/org/thoughtcrime/securesms/util/CharacterCalculator.java
+++ b/src/org/thoughtcrime/securesms/util/CharacterCalculator.java
@@ -45,14 +45,16 @@ public abstract class CharacterCalculator {
}
public static class CharacterState {
- public int charactersRemaining;
- public int messagesSpent;
- public int maxMessageSize;
+ public final int charactersRemaining;
+ public final int messagesSpent;
+ public final int maxTotalMessageSize;
+ public final int maxPrimaryMessageSize;
- public CharacterState(int messagesSpent, int charactersRemaining, int maxMessageSize) {
- this.messagesSpent = messagesSpent;
- this.charactersRemaining = charactersRemaining;
- this.maxMessageSize = maxMessageSize;
+ public CharacterState(int messagesSpent, int charactersRemaining, int maxTotalMessageSize, int maxPrimaryMessageSize) {
+ this.messagesSpent = messagesSpent;
+ this.charactersRemaining = charactersRemaining;
+ this.maxTotalMessageSize = maxTotalMessageSize;
+ this.maxPrimaryMessageSize = maxPrimaryMessageSize;
}
}
}
diff --git a/src/org/thoughtcrime/securesms/util/MediaUtil.java b/src/org/thoughtcrime/securesms/util/MediaUtil.java
index c09959c3f7..b0afcce392 100644
--- a/src/org/thoughtcrime/securesms/util/MediaUtil.java
+++ b/src/org/thoughtcrime/securesms/util/MediaUtil.java
@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.MmsSlide;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
+import org.thoughtcrime.securesms.mms.TextSlide;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
@@ -45,6 +46,7 @@ public class MediaUtil {
public static final String AUDIO_UNSPECIFIED = "audio/*";
public static final String VIDEO_UNSPECIFIED = "video/*";
public static final String VCARD = "text/x-vcard";
+ public static final String LONG_TEXT = "text/x-signal-plain";
public static Slide getSlideForAttachment(Context context, Attachment attachment) {
@@ -59,6 +61,8 @@ public class MediaUtil {
slide = new AudioSlide(context, attachment);
} else if (isMms(attachment.getContentType())) {
slide = new MmsSlide(context, attachment);
+ } else if (isLongTextType(attachment.getContentType())) {
+ slide = new TextSlide(context, attachment);
} else if (attachment.getContentType() != null) {
slide = new DocumentSlide(context, attachment);
}
@@ -230,6 +234,10 @@ public class MediaUtil {
return (null != contentType) && contentType.startsWith("video/");
}
+ public static boolean isLongTextType(String contentType) {
+ return (null != contentType) && contentType.equals(LONG_TEXT);
+ }
+
public static boolean hasVideoThumbnail(Uri uri) {
Log.i(TAG, "Checking: " + uri);
diff --git a/src/org/thoughtcrime/securesms/util/MmsCharacterCalculator.java b/src/org/thoughtcrime/securesms/util/MmsCharacterCalculator.java
index 9d4698c207..c25d47a845 100644
--- a/src/org/thoughtcrime/securesms/util/MmsCharacterCalculator.java
+++ b/src/org/thoughtcrime/securesms/util/MmsCharacterCalculator.java
@@ -6,6 +6,6 @@ public class MmsCharacterCalculator extends CharacterCalculator {
@Override
public CharacterState calculateCharacters(String messageBody) {
- return new CharacterState(1, MAX_SIZE - messageBody.length(), MAX_SIZE);
+ return new CharacterState(1, MAX_SIZE - messageBody.length(), MAX_SIZE, MAX_SIZE);
}
}
diff --git a/src/org/thoughtcrime/securesms/util/PushCharacterCalculator.java b/src/org/thoughtcrime/securesms/util/PushCharacterCalculator.java
index 0faa63920b..452f3851ef 100644
--- a/src/org/thoughtcrime/securesms/util/PushCharacterCalculator.java
+++ b/src/org/thoughtcrime/securesms/util/PushCharacterCalculator.java
@@ -17,10 +17,13 @@
package org.thoughtcrime.securesms.util;
public class PushCharacterCalculator extends CharacterCalculator {
- private static final int MAX_SIZE = 2000;
+ // TODO: Switch to 64kb to enable long message sending.
+// private static final int MAX_TOTAL_SIZE = 64 * 1024;
+ private static final int MAX_TOTAL_SIZE = 2000;
+ private static final int MAX_PRIMARY_SIZE = 2000;
@Override
public CharacterState calculateCharacters(String messageBody) {
- return new CharacterState(1, MAX_SIZE - messageBody.length(), MAX_SIZE);
+ return new CharacterState(1, MAX_TOTAL_SIZE - messageBody.length(), MAX_TOTAL_SIZE, MAX_PRIMARY_SIZE);
}
}
diff --git a/src/org/thoughtcrime/securesms/util/SmsCharacterCalculator.java b/src/org/thoughtcrime/securesms/util/SmsCharacterCalculator.java
index d7ca110bda..96251e768e 100644
--- a/src/org/thoughtcrime/securesms/util/SmsCharacterCalculator.java
+++ b/src/org/thoughtcrime/securesms/util/SmsCharacterCalculator.java
@@ -50,7 +50,7 @@ public class SmsCharacterCalculator extends CharacterCalculator {
maxMessageSize = (charactersSpent + charactersRemaining);
}
- return new CharacterState(messagesSpent, charactersRemaining, maxMessageSize);
+ return new CharacterState(messagesSpent, charactersRemaining, maxMessageSize, maxMessageSize);
}
}
diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java
index c9fc0a4f5d..3c0400c7ef 100644
--- a/src/org/thoughtcrime/securesms/util/Util.java
+++ b/src/org/thoughtcrime/securesms/util/Util.java
@@ -142,6 +142,15 @@ public class Util {
return map.containsKey(key) ? map.get(key) : defaultValue;
}
+ public static String getFirstNonEmpty(String... values) {
+ for (String value : values) {
+ if (!TextUtils.isEmpty(value)) {
+ return value;
+ }
+ }
+ return "";
+ }
+
public static List> chunk(@NonNull List list, int chunkSize) {
List> chunks = new ArrayList<>(list.size() / chunkSize);