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" /> android:value="org.thoughtcrime.securesms.ConversationListActivity" />
</activity> </activity>
<activity android:name=".longmessage.LongMessageActivity" />
<activity android:name=".conversation.ConversationPopupActivity" <activity android:name=".conversation.ConversationPopupActivity"
android:windowSoftInputMode="stateVisible" android:windowSoftInputMode="stateVisible"
android:launchMode="singleTask" android:launchMode="singleTask"

View File

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

View File

@ -102,7 +102,9 @@
style="@style/Signal.Text.Body" style="@style/Signal.Text.Body"
android:textColor="?conversation_item_sent_text_primary_color" android:textColor="?conversation_item_sent_text_primary_color"
android:textColorLink="?conversation_item_sent_text_primary_color" android:textColorLink="?conversation_item_sent_text_primary_color"
android:ellipsize="end"
app:scaleEmojis="true" app:scaleEmojis="true"
app:emoji_maxLength="1000"
tools:text="Mango pickle lorem ipsum"/> tools:text="Mango pickle lorem ipsum"/>
<View <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"> <declare-styleable name="EmojiTextView">
<attr name="scaleEmojis" format="boolean" /> <attr name="scaleEmojis" format="boolean" />
<attr name="emoji_maxLength" format="integer" />
</declare-styleable> </declare-styleable>
<declare-styleable name="RingtonePreference"> <declare-styleable name="RingtonePreference">

View File

@ -133,6 +133,9 @@
<string name="ConversationItem_copied_text">Copied %s</string> <string name="ConversationItem_copied_text">Copied %s</string>
<string name="ConversationItem_from_s">from %s</string> <string name="ConversationItem_from_s">from %s</string>
<string name="ConversationItem_to_s">to %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 --> <!-- ConversationActivity -->
<string name="ConversationActivity_reset_secure_session_question">Reset secure session?</string> <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_failed_to_send">Failed to send</string>
<string name="MessageDetailsRecipient_new_safety_number">New safety number</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 --> <!-- MessageRetrievalService -->
<string name="MessageRetrievalService_signal">Signal</string> <string name="MessageRetrievalService_signal">Signal</string>
<string name="MessageRetrievalService_background_connection_enabled">Background connection enabled</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:padding">2dp</item>
<item name="android:background">@null</item> <item name="android:background">@null</item>
<item name="android:maxLines">4</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:maxLength">2000</item>
<item name="android:textColor">?conversation_item_sent_text_primary_color</item> <item name="android:textColor">?conversation_item_sent_text_primary_color</item>
<item name="android:capitalize">sentences</item> <item name="android:capitalize">sentences</item>

View File

@ -5,6 +5,7 @@ import android.support.annotation.Nullable;
import android.view.View; import android.view.View;
import org.thoughtcrime.securesms.contactshare.Contact; 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.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreview;
@ -34,6 +35,7 @@ public interface BindableConversationItem extends Unbindable {
interface EventListener { interface EventListener {
void onQuoteClicked(MmsMessageRecord messageRecord); void onQuoteClicked(MmsMessageRecord messageRecord);
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview); void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
void onMoreTextClicked(@NonNull Address conversationAddress, long messageId, boolean isMms);
void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView); void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView);
void onAddToContactsClicked(@NonNull Contact contact); void onAddToContactsClicked(@NonNull Contact contact);
void onMessageSharedContactClicked(@NonNull List<Recipient> choices); 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.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
public class EmojiTextView extends AppCompatTextView { public class EmojiTextView extends AppCompatTextView {
private final boolean scaleEmojis; private final boolean scaleEmojis;
private static final char ELLIPSIS = '…';
private CharSequence previousText; private CharSequence previousText;
private BufferType previousBufferType; private BufferType previousBufferType;
private float originalFontSize; private float originalFontSize;
private boolean useSystemEmoji; private boolean useSystemEmoji;
private boolean sizeChangeInProgress; private boolean sizeChangeInProgress;
private int maxLength;
private CharSequence overflowText;
private CharSequence previousOverflowText;
public EmojiTextView(Context context) { public EmojiTextView(Context context) {
this(context, null); this(context, null);
@ -42,6 +48,7 @@ public class EmojiTextView extends AppCompatTextView {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false); scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
a.recycle(); a.recycle();
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize}); 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); super.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalFontSize);
} }
if (unchanged(text, type)) { if (unchanged(text, overflowText, type)) {
return; return;
} }
previousText = text; previousText = text;
previousBufferType = type; previousOverflowText = overflowText;
useSystemEmoji = useSystemEmoji(); previousBufferType = type;
useSystemEmoji = useSystemEmoji();
if (useSystemEmoji || candidates == null || candidates.size() == 0) { if (maxLength <= 0 && (useSystemEmoji || candidates == null || candidates.size() == 0)) {
super.setText(text, BufferType.NORMAL); super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")).append(Optional.fromNullable(overflowText).or("")), BufferType.NORMAL);
return; return;
} }
CharSequence emojified = provider.emojify(candidates, text, this); 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) // Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
// We ellipsize them ourselves by manually truncating the appropriate section. // 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() { 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(() -> { post(() -> {
if (getLayout() == null) { if (getLayout() == null) {
ellipsize(); ellipsize();
@ -98,7 +122,7 @@ public class EmojiTextView extends AppCompatTextView {
} }
int maxLines = TextViewCompat.getMaxLines(EmojiTextView.this); int maxLines = TextViewCompat.getMaxLines(EmojiTextView.this);
if (maxLines <= 0) { if (maxLines <= 0 && maxLength < 0) {
return; return;
} }
@ -110,7 +134,8 @@ public class EmojiTextView extends AppCompatTextView {
SpannableStringBuilder newContent = new SpannableStringBuilder(); SpannableStringBuilder newContent = new SpannableStringBuilder();
newContent.append(getText().subSequence(0, overflowStart)) 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); EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent);
CharSequence emojified = EmojiProvider.getInstance(getContext()).emojify(newCandidates, newContent, this); 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) { private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) {
return Util.equals(previousText, text) && return Util.equals(previousText, text) &&
Util.equals(previousBufferType, bufferType) && Util.equals(previousOverflowText, overflowText) &&
useSystemEmoji == useSystemEmoji() && Util.equals(previousBufferType, bufferType) &&
useSystemEmoji == useSystemEmoji() &&
!sizeChangeInProgress; !sizeChangeInProgress;
} }

View File

@ -170,12 +170,14 @@ import org.thoughtcrime.securesms.mms.QuoteId;
import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.TextSlide;
import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.GroupShareProfileView; import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
import org.thoughtcrime.securesms.providers.MemoryBlobProvider;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider; import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
@ -212,9 +214,12 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -1905,7 +1910,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
charactersLeft.setText(String.format(dynamicLanguage.getCurrentLocale(), charactersLeft.setText(String.format(dynamicLanguage.getCurrentLocale(),
"%d/%d (%d)", "%d/%d (%d)",
characterState.charactersRemaining, characterState.charactersRemaining,
characterState.maxMessageSize, characterState.maxTotalMessageSize,
characterState.messagesSpent)); characterState.messagesSpent));
charactersLeft.setVisibility(View.VISIBLE); charactersLeft.setVisibility(View.VISIBLE);
} else { } else {
@ -1961,6 +1966,24 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return rawText; 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() { private MediaConstraints getCurrentMediaConstraints() {
return sendButton.getSelectedTransport().getType() == Type.TEXTSECURE return sendButton.getSelectedTransport().getType() == Type.TEXTSECURE
? MediaConstraints.getPushMediaConstraints() ? MediaConstraints.getPushMediaConstraints()
@ -2021,6 +2044,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
throw new RecipientFormattingException("Badly formatted"); throw new RecipientFormattingException("Badly formatted");
} }
String message = getMessage();
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms(); boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1); int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
long expiresIn = recipient.getExpireMessages() * 1000L; long expiresIn = recipient.getExpireMessages() * 1000L;
@ -2029,7 +2053,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
recipient.isGroupRecipient() || recipient.isGroupRecipient() ||
recipient.getAddress().isEmail() || recipient.getAddress().isEmail() ||
inputPanel.getQuote().isPresent() || inputPanel.getQuote().isPresent() ||
linkPreviewViewModel.hasLinkPreview(); linkPreviewViewModel.hasLinkPreview() ||
message.length() > sendButton.getSelectedTransport().calculateCharacters(message).maxPrimaryMessageSize;
Log.i(TAG, "isManual Selection: " + sendButton.isManualSelection()); Log.i(TAG, "isManual Selection: " + sendButton.isManualSelection());
Log.i(TAG, "forceSms: " + forceSms); Log.i(TAG, "forceSms: " + forceSms);
@ -2078,6 +2103,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return new SettableFuture<>(null); 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); OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType, inputPanel.getQuote().orNull(), contacts, previews);
final SettableFuture<Void> future = new SettableFuture<>(); 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.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.profiles.UnknownSenderView; import org.thoughtcrime.securesms.profiles.UnknownSenderView;
import org.thoughtcrime.securesms.recipients.Recipient; 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.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@ -516,43 +520,60 @@ public class ConversationFragment extends Fragment
} }
private void handleForwardMessage(MessageRecord message) { private void handleForwardMessage(MessageRecord message) {
Intent composeIntent = new Intent(getActivity(), ShareActivity.class); SimpleTask.run(getLifecycle(), () -> {
composeIntent.putExtra(Intent.EXTRA_TEXT, message.getDisplayBody().toString()); Intent composeIntent = new Intent(getActivity(), ShareActivity.class);
if (message.isMms()) { composeIntent.putExtra(Intent.EXTRA_TEXT, message.getDisplayBody().toString());
MmsMessageRecord mediaMessage = (MmsMessageRecord) message;
boolean isAlbum = mediaMessage.containsMediaSlide() &&
mediaMessage.getSlideDeck().getSlides().size() > 1 &&
mediaMessage.getSlideDeck().getAudioSlide() == null &&
mediaMessage.getSlideDeck().getDocumentSlide() == null;
if (isAlbum) { if (message.isMms()) {
ArrayList<Media> mediaList = new ArrayList<>(mediaMessage.getSlideDeck().getSlides().size()); 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()) { if (isAlbum) {
Uri uri = attachment.getDataUri() != null ? attachment.getDataUri() : attachment.getThumbnailUri(); 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();
if (uri != null) { for (Attachment attachment : attachments) {
mediaList.add(new Media(uri, Uri uri = attachment.getDataUri() != null ? attachment.getDataUri() : attachment.getThumbnailUri();
attachment.getContentType(),
System.currentTimeMillis(), if (uri != null) {
attachment.getWidth(), mediaList.add(new Media(uri,
attachment.getHeight(), attachment.getContentType(),
attachment.getSize(), System.currentTimeMillis(),
Optional.absent(), attachment.getWidth(),
Optional.fromNullable(attachment.getCaption()))); 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) { 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 @Override
public void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView) { public void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView) {
if (getContext() != null && getActivity() != null) { if (getContext() != null && getActivity() != null) {

View File

@ -23,6 +23,7 @@ import android.content.Intent;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.Typeface;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.DimenRes; import android.support.annotation.DimenRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@ -30,9 +31,13 @@ import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.BackgroundColorSpan; import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.text.util.Linkify; import android.text.util.Linkify;
@ -46,6 +51,8 @@ import org.thoughtcrime.securesms.MessageDetailsActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.LinkPreviewView; 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.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log; 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.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.mms.TextSlide;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.util.DateUtils; 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 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 MessageRecord messageRecord;
private Locale locale; private Locale locale;
@ -129,7 +138,7 @@ public class ConversationItem extends LinearLayout
protected ViewGroup bodyBubble; protected ViewGroup bodyBubble;
private QuoteView quoteView; private QuoteView quoteView;
private TextView bodyText; private EmojiTextView bodyText;
private ConversationItemFooter footer; private ConversationItemFooter footer;
private TextView groupSender; private TextView groupSender;
private TextView groupSenderProfileName; private TextView groupSenderProfileName;
@ -378,7 +387,7 @@ public class ConversationItem extends LinearLayout
} }
private boolean isCaptionlessMms(MessageRecord messageRecord) { 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) { private boolean hasAudio(MessageRecord messageRecord) {
@ -397,6 +406,13 @@ public class ConversationItem extends LinearLayout
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null; 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) { private boolean hasQuote(MessageRecord messageRecord) {
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getQuote() != null; 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 BackgroundColorSpan(Color.YELLOW), styledText, searchQuery);
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), 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.setText(styledText);
bodyText.setVisibility(View.VISIBLE); bodyText.setVisibility(View.VISIBLE);
} }
@ -536,7 +558,7 @@ public class ConversationItem extends LinearLayout
mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener); mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener);
mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener); mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener);
mediaThumbnailStub.get().setOnClickListener(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 mediaThumbnailStub.get().setConversationColor(messageRecord.isOutgoing() ? defaultBubbleColor
: messageRecord.getRecipient().getColor().toConversationColor(context)); : messageRecord.getRecipient().getColor().toConversationColor(context));
@ -613,7 +635,7 @@ public class ConversationItem extends LinearLayout
topRight = 0; topRight = 0;
} }
if (hasLinkPreview(messageRecord)) { if (hasLinkPreview(messageRecord) || hasExtraText(messageRecord)) {
bottomLeft = 0; bottomLeft = 0;
bottomRight = 0; bottomRight = 0;
} }
@ -748,7 +770,7 @@ public class ConversationItem extends LinearLayout
ViewUtil.updateLayoutParams(footer, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); ViewUtil.updateLayoutParams(footer, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
footer.setVisibility(GONE); 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); if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().getFooter().setVisibility(GONE);
boolean differentTimestamps = next.isPresent() && !DateUtils.isSameExtendedRelativeTimestamp(context, locale, next.get().getTimestamp(), current.getTimestamp()); 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(); 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 @Override
public void onModified(final Recipient modified) { public void onModified(final Recipient modified) {
Util.runOnMain(() -> { Util.runOnMain(() -> {

View File

@ -51,7 +51,10 @@ public class MediaDatabase extends Database {
+ "ORDER BY " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " DESC"; + "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 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) { MediaDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper); super(context, databaseHelper);

View File

@ -43,8 +43,6 @@ import java.util.List;
*/ */
public abstract class MessageRecord extends DisplayRecord { public abstract class MessageRecord extends DisplayRecord {
private static final int MAX_DISPLAY_LENGTH = 2000;
private final Recipient individualRecipient; private final Recipient individualRecipient;
private final int recipientDeviceId; private final int recipientDeviceId;
private final long id; private final long id;
@ -123,8 +121,6 @@ public abstract class MessageRecord extends DisplayRecord {
} else if (isIdentityDefault()) { } else if (isIdentityDefault()) {
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().toShortString())); 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 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()); 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; package org.thoughtcrime.securesms.mediasend;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.arch.lifecycle.ViewModelProviders; import android.arch.lifecycle.ViewModelProviders;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@ -358,7 +357,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
charactersLeft.setText(String.format(locale, charactersLeft.setText(String.format(locale,
"%d/%d (%d)", "%d/%d (%d)",
characterState.charactersRemaining, characterState.charactersRemaining,
characterState.maxMessageSize, characterState.maxTotalMessageSize,
characterState.messagesSpent)); characterState.messagesSpent));
charactersLeft.setVisibility(View.VISIBLE); charactersLeft.setVisibility(View.VISIBLE);
} else { } else {

View File

@ -128,4 +128,14 @@ public class SlideDeck {
return null; 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, charactersLeft.setText(String.format(locale,
"%d/%d (%d)", "%d/%d (%d)",
characterState.charactersRemaining, characterState.charactersRemaining,
characterState.maxMessageSize, characterState.maxTotalMessageSize,
characterState.messagesSpent)); characterState.messagesSpent));
charactersLeft.setVisibility(View.VISIBLE); charactersLeft.setVisibility(View.VISIBLE);
} else { } else {

View File

@ -37,7 +37,7 @@ public class AttachmentUtil {
Set<String> allowedTypes = getAllowedAutoDownloadTypes(context); Set<String> allowedTypes = getAllowedAutoDownloadTypes(context);
String contentType = attachment.getContentType(); 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; return true;
} else if (isNonDocumentType(contentType)) { } else if (isNonDocumentType(contentType)) {
return allowedTypes.contains(MediaUtil.getDiscreteMimeType(contentType)); return allowedTypes.contains(MediaUtil.getDiscreteMimeType(contentType));

View File

@ -45,14 +45,16 @@ public abstract class CharacterCalculator {
} }
public static class CharacterState { public static class CharacterState {
public int charactersRemaining; public final int charactersRemaining;
public int messagesSpent; public final int messagesSpent;
public int maxMessageSize; 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.messagesSpent = messagesSpent;
this.charactersRemaining = charactersRemaining; 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.MmsSlide;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.TextSlide;
import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider; import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
@ -45,6 +46,7 @@ public class MediaUtil {
public static final String AUDIO_UNSPECIFIED = "audio/*"; public static final String AUDIO_UNSPECIFIED = "audio/*";
public static final String VIDEO_UNSPECIFIED = "video/*"; public static final String VIDEO_UNSPECIFIED = "video/*";
public static final String VCARD = "text/x-vcard"; 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) { public static Slide getSlideForAttachment(Context context, Attachment attachment) {
@ -59,6 +61,8 @@ public class MediaUtil {
slide = new AudioSlide(context, attachment); slide = new AudioSlide(context, attachment);
} else if (isMms(attachment.getContentType())) { } else if (isMms(attachment.getContentType())) {
slide = new MmsSlide(context, attachment); slide = new MmsSlide(context, attachment);
} else if (isLongTextType(attachment.getContentType())) {
slide = new TextSlide(context, attachment);
} else if (attachment.getContentType() != null) { } else if (attachment.getContentType() != null) {
slide = new DocumentSlide(context, attachment); slide = new DocumentSlide(context, attachment);
} }
@ -230,6 +234,10 @@ public class MediaUtil {
return (null != contentType) && contentType.startsWith("video/"); 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) { public static boolean hasVideoThumbnail(Uri uri) {
Log.i(TAG, "Checking: " + uri); Log.i(TAG, "Checking: " + uri);

View File

@ -6,6 +6,6 @@ public class MmsCharacterCalculator extends CharacterCalculator {
@Override @Override
public CharacterState calculateCharacters(String messageBody) { 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; package org.thoughtcrime.securesms.util;
public class PushCharacterCalculator extends CharacterCalculator { 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 @Override
public CharacterState calculateCharacters(String messageBody) { 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); 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; 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) { public static <E> List<List<E>> chunk(@NonNull List<E> list, int chunkSize) {
List<List<E>> chunks = new ArrayList<>(list.size() / chunkSize); List<List<E>> chunks = new ArrayList<>(list.size() / chunkSize);