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);