Added ability to receive long messages.

Send support is in here too. We'll enable it in a future release after
enough people have updated.
This commit is contained in:
Greyson Parrelli 2019-02-26 19:29:52 -08:00
parent bf28e109d3
commit 55699e27bc
31 changed files with 764 additions and 73 deletions

View File

@ -218,6 +218,8 @@
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
</activity>
<activity android:name=".longmessage.LongMessageActivity" />
<activity android:name=".conversation.ConversationPopupActivity"
android:windowSoftInputMode="stateVisible"
android:launchMode="singleTask"

View File

@ -165,7 +165,9 @@
style="@style/Signal.Text.Body"
android:textColor="?conversation_item_received_text_primary_color"
android:textColorLink="?conversation_item_received_text_primary_color"
android:ellipsize="end"
app:scaleEmojis="true"
app:emoji_maxLength="1000"
tools:text="Mango pickle lorem ipsum"/>
<org.thoughtcrime.securesms.components.ConversationItemFooter

View File

@ -102,7 +102,9 @@
style="@style/Signal.Text.Body"
android:textColor="?conversation_item_sent_text_primary_color"
android:textColorLink="?conversation_item_sent_text_primary_color"
android:ellipsize="end"
app:scaleEmojis="true"
app:emoji_maxLength="1000"
tools:text="Mango pickle lorem ipsum"/>
<View

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<ViewStub
android:id="@+id/longmessage_sent_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/longmessage_bubble_sent"/>
<ViewStub
android:id="@+id/longmessage_received_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/longmessage_bubble_received"/>
</FrameLayout>
</ScrollView>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/message_bubble_background_received_alone"
android:visibility="gone"
tools:visibility="visible">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/longmessage_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
style="@style/Signal.Text.Body"
android:textColor="?conversation_item_received_text_primary_color"
android:textColorLink="?conversation_item_received_text_primary_color"
android:textIsSelectable="true"
app:scaleEmojis="true"
tools:text="With great power comes great responsibility."/>
<org.thoughtcrime.securesms.components.ConversationItemFooter
android:id="@+id/longmessage_footer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-4dp"
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
android:clipChildren="false"
android:clipToPadding="false"
android:alpha="0.7"
app:footer_text_color="?attr/conversation_item_received_text_secondary_color"
app:footer_icon_color="?attr/conversation_item_received_text_secondary_color"/>
</LinearLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/message_bubble_background_sent_alone"
android:visibility="gone"
tools:visibility="visible">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/longmessage_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
style="@style/Signal.Text.Body"
android:textColor="?conversation_item_sent_text_primary_color"
android:textColorLink="?conversation_item_sent_text_primary_color"
android:textIsSelectable="true"
app:scaleEmojis="true"
tools:text="With great power comes great responsibility."/>
<org.thoughtcrime.securesms.components.ConversationItemFooter
android:id="@+id/longmessage_footer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-4dp"
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
android:layout_marginRight="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
android:clipChildren="false"
android:clipToPadding="false"
app:footer_text_color="?attr/conversation_item_sent_text_secondary_color"
app:footer_icon_color="?attr/conversation_item_sent_icon_color"/>
</LinearLayout>

View File

@ -236,6 +236,7 @@
<declare-styleable name="EmojiTextView">
<attr name="scaleEmojis" format="boolean" />
<attr name="emoji_maxLength" format="integer" />
</declare-styleable>
<declare-styleable name="RingtonePreference">

View File

@ -133,6 +133,9 @@
<string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_from_s">from %s</string>
<string name="ConversationItem_to_s">to %s</string>
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<!-- ConversationActivity -->
<string name="ConversationActivity_reset_secure_session_question">Reset secure session?</string>
@ -409,6 +412,11 @@
<string name="MessageDetailsRecipient_failed_to_send">Failed to send</string>
<string name="MessageDetailsRecipient_new_safety_number">New safety number</string>
<!-- LongMessageActivity -->
<string name="LongMessageActivity_unable_to_find_message">Unable to find message</string>
<string name="LongMessageActivity_message_from_s">Message from %1$s</string>
<string name="LongMessageActivity_your_message">Your message</string>
<!-- MessageRetrievalService -->
<string name="MessageRetrievalService_signal">Signal</string>
<string name="MessageRetrievalService_background_connection_enabled">Background connection enabled</string>

View File

@ -173,6 +173,8 @@
<item name="android:padding">2dp</item>
<item name="android:background">@null</item>
<item name="android:maxLines">4</item>
<!-- TODO: Switch to 64kb to enable long message sending. -->
<!--<item name="android:maxLength">65536</item>-->
<item name="android:maxLength">2000</item>
<item name="android:textColor">?conversation_item_sent_text_primary_color</item>
<item name="android:capitalize">sentences</item>

View File

@ -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<Recipient> choices);

View File

@ -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;
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,7 +122,7 @@ public class EmojiTextView extends AppCompatTextView {
}
int maxLines = TextViewCompat.getMaxLines(EmojiTextView.this);
if (maxLines <= 0) {
if (maxLines <= 0 && maxLength < 0) {
return;
}
@ -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,8 +145,9 @@ public class EmojiTextView extends AppCompatTextView {
});
}
private boolean unchanged(CharSequence text, BufferType bufferType) {
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;

View File

@ -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<String, Optional<Slide>> getSplitMessage(String rawText, int maxPrimaryMessageSize) {
String bodyText = rawText;
Optional<Slide> 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<String, Optional<Slide>> 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<Void> future = new SettableFuture<>();

View File

@ -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,8 +520,10 @@ public class ConversationFragment extends Fragment
}
private void handleForwardMessage(MessageRecord message) {
SimpleTask.run(getLifecycle(), () -> {
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() &&
@ -527,8 +533,12 @@ public class ConversationFragment extends Fragment
if (isAlbum) {
ArrayList<Media> mediaList = new ArrayList<>(mediaMessage.getSlideDeck().getSlides().size());
List<Attachment> attachments = Stream.of(mediaMessage.getSlideDeck().getSlides())
.filter(s -> s.hasImage() || s.hasVideo())
.map(Slide::asAttachment)
.toList();
for (Attachment attachment : mediaMessage.getSlideDeck().asAttachments()) {
for (Attachment attachment : attachments) {
Uri uri = attachment.getDataUri() != null ? attachment.getDataUri() : attachment.getThumbnailUri();
if (uri != null) {
@ -541,7 +551,7 @@ public class ConversationFragment extends Fragment
Optional.absent(),
Optional.fromNullable(attachment.getCaption())));
}
}
};
if (!mediaList.isEmpty()) {
composeIntent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaList);
@ -551,8 +561,19 @@ public class ConversationFragment extends Fragment
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.");
}
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) {

View File

@ -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;
@ -120,6 +128,7 @@ 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_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;
}
@ -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(() -> {

View File

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

View File

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

View File

@ -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;
}
}

View File

@ -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<ViewGroup> sentBubble;
private Stub<ViewGroup> 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());
});
}
}

View File

@ -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<Optional<LongMessage>> callback) {
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
if (isMms) {
callback.onComplete(getMmsLongMessage(context, mmsDatabase, messageId));
} else {
callback.onComplete(getSmsLongMessage(smsDatabase, messageId));
}
});
}
@WorkerThread
private Optional<LongMessage> getMmsLongMessage(@NonNull Context context, @NonNull MmsDatabase mmsDatabase, long messageId) {
Optional<MmsMessageRecord> 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<LongMessage> getSmsLongMessage(@NonNull SmsDatabase smsDatabase, long messageId) {
Optional<MessageRecord> record = getSmsMessage(smsDatabase, messageId);
if (record.isPresent()) {
return Optional.of(new LongMessage(record.get(), ""));
} else {
return Optional.absent();
}
}
@WorkerThread
private Optional<MmsMessageRecord> getMmsMessage(@NonNull MmsDatabase mmsDatabase, long messageId) {
try (Cursor cursor = mmsDatabase.getMessage(messageId)) {
return Optional.fromNullable((MmsMessageRecord) mmsDatabase.readerFor(cursor).getNext());
}
}
@WorkerThread
private Optional<MessageRecord> 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<T> {
void onComplete(T result);
}
}

View File

@ -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<Optional<LongMessage>> 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<Optional<LongMessage>> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
return modelClass.cast(new LongMessageViewModel(context, repository, messageId, isMms));
}
}
}

View File

@ -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 {

View File

@ -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;
}
}

View File

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

View File

@ -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 {

View File

@ -37,7 +37,7 @@ public class AttachmentUtil {
Set<String> 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));

View File

@ -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) {
public CharacterState(int messagesSpent, int charactersRemaining, int maxTotalMessageSize, int maxPrimaryMessageSize) {
this.messagesSpent = messagesSpent;
this.charactersRemaining = charactersRemaining;
this.maxMessageSize = maxMessageSize;
this.maxTotalMessageSize = maxTotalMessageSize;
this.maxPrimaryMessageSize = maxPrimaryMessageSize;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <E> List<List<E>> chunk(@NonNull List<E> list, int chunkSize) {
List<List<E>> chunks = new ArrayList<>(list.size() / chunkSize);