mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
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:
parent
bf28e109d3
commit
55699e27bc
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
26
res/layout/longmessage_activity.xml
Normal file
26
res/layout/longmessage_activity.xml
Normal 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>
|
42
res/layout/longmessage_bubble_received.xml
Normal file
42
res/layout/longmessage_bubble_received.xml
Normal 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>
|
41
res/layout/longmessage_bubble_sent.xml
Normal file
41
res/layout/longmessage_bubble_sent.xml
Normal 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>
|
@ -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">
|
||||
|
@ -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">  Read More</string>
|
||||
<string name="ConversationItem_download_more">  Download More</string>
|
||||
<string name="ConversationItem_pending">  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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -17,17 +17,23 @@ import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
|
||||
public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
private final boolean scaleEmojis;
|
||||
|
||||
private static final char ELLIPSIS = '…';
|
||||
|
||||
private CharSequence previousText;
|
||||
private BufferType previousBufferType;
|
||||
private float originalFontSize;
|
||||
private boolean useSystemEmoji;
|
||||
private boolean sizeChangeInProgress;
|
||||
private int maxLength;
|
||||
private CharSequence overflowText;
|
||||
private CharSequence previousOverflowText;
|
||||
|
||||
public EmojiTextView(Context context) {
|
||||
this(context, null);
|
||||
@ -42,6 +48,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
|
||||
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
|
||||
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
|
||||
a.recycle();
|
||||
|
||||
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
|
||||
@ -67,21 +74,22 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalFontSize);
|
||||
}
|
||||
|
||||
if (unchanged(text, type)) {
|
||||
if (unchanged(text, overflowText, type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
previousText = text;
|
||||
previousBufferType = type;
|
||||
useSystemEmoji = useSystemEmoji();
|
||||
previousText = text;
|
||||
previousOverflowText = overflowText;
|
||||
previousBufferType = type;
|
||||
useSystemEmoji = useSystemEmoji();
|
||||
|
||||
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
|
||||
super.setText(text, BufferType.NORMAL);
|
||||
if (maxLength <= 0 && (useSystemEmoji || candidates == null || candidates.size() == 0)) {
|
||||
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")).append(Optional.fromNullable(overflowText).or("")), BufferType.NORMAL);
|
||||
return;
|
||||
}
|
||||
|
||||
CharSequence emojified = provider.emojify(candidates, text, this);
|
||||
super.setText(emojified, BufferType.SPANNABLE);
|
||||
super.setText(new SpannableStringBuilder(emojified).append(Optional.fromNullable(overflowText).or("")), BufferType.SPANNABLE);
|
||||
|
||||
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
|
||||
// We ellipsize them ourselves by manually truncating the appropriate section.
|
||||
@ -90,7 +98,23 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
}
|
||||
|
||||
public void setOverflowText(@Nullable CharSequence overflowText) {
|
||||
this.overflowText = overflowText;
|
||||
setText(previousText, BufferType.SPANNABLE);
|
||||
}
|
||||
|
||||
private void ellipsize() {
|
||||
if (maxLength > 0 && getText().length() > maxLength + 1) {
|
||||
SpannableStringBuilder newContent = new SpannableStringBuilder();
|
||||
newContent.append(getText().subSequence(0, maxLength)).append(ELLIPSIS).append(Optional.fromNullable(overflowText).or(""));
|
||||
|
||||
EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent);
|
||||
CharSequence emojified = EmojiProvider.getInstance(getContext()).emojify(newCandidates, newContent, this);
|
||||
|
||||
super.setText(emojified, BufferType.SPANNABLE);
|
||||
return;
|
||||
}
|
||||
|
||||
post(() -> {
|
||||
if (getLayout() == null) {
|
||||
ellipsize();
|
||||
@ -98,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,10 +145,11 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
});
|
||||
}
|
||||
|
||||
private boolean unchanged(CharSequence text, BufferType bufferType) {
|
||||
return Util.equals(previousText, text) &&
|
||||
Util.equals(previousBufferType, bufferType) &&
|
||||
useSystemEmoji == useSystemEmoji() &&
|
||||
private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) {
|
||||
return Util.equals(previousText, text) &&
|
||||
Util.equals(previousOverflowText, overflowText) &&
|
||||
Util.equals(previousBufferType, bufferType) &&
|
||||
useSystemEmoji == useSystemEmoji() &&
|
||||
!sizeChangeInProgress;
|
||||
}
|
||||
|
||||
|
@ -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<>();
|
||||
|
@ -82,9 +82,11 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.profiles.UnknownSenderView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@ -100,6 +102,8 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
@ -516,43 +520,60 @@ public class ConversationFragment extends Fragment
|
||||
}
|
||||
|
||||
private void handleForwardMessage(MessageRecord message) {
|
||||
Intent composeIntent = new Intent(getActivity(), ShareActivity.class);
|
||||
composeIntent.putExtra(Intent.EXTRA_TEXT, message.getDisplayBody().toString());
|
||||
if (message.isMms()) {
|
||||
MmsMessageRecord mediaMessage = (MmsMessageRecord) message;
|
||||
boolean isAlbum = mediaMessage.containsMediaSlide() &&
|
||||
mediaMessage.getSlideDeck().getSlides().size() > 1 &&
|
||||
mediaMessage.getSlideDeck().getAudioSlide() == null &&
|
||||
mediaMessage.getSlideDeck().getDocumentSlide() == null;
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
Intent composeIntent = new Intent(getActivity(), ShareActivity.class);
|
||||
composeIntent.putExtra(Intent.EXTRA_TEXT, message.getDisplayBody().toString());
|
||||
|
||||
if (isAlbum) {
|
||||
ArrayList<Media> mediaList = new ArrayList<>(mediaMessage.getSlideDeck().getSlides().size());
|
||||
if (message.isMms()) {
|
||||
MmsMessageRecord mediaMessage = (MmsMessageRecord) message;
|
||||
boolean isAlbum = mediaMessage.containsMediaSlide() &&
|
||||
mediaMessage.getSlideDeck().getSlides().size() > 1 &&
|
||||
mediaMessage.getSlideDeck().getAudioSlide() == null &&
|
||||
mediaMessage.getSlideDeck().getDocumentSlide() == null;
|
||||
|
||||
for (Attachment attachment : mediaMessage.getSlideDeck().asAttachments()) {
|
||||
Uri uri = attachment.getDataUri() != null ? attachment.getDataUri() : attachment.getThumbnailUri();
|
||||
if (isAlbum) {
|
||||
ArrayList<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) {
|
||||
mediaList.add(new Media(uri,
|
||||
attachment.getContentType(),
|
||||
System.currentTimeMillis(),
|
||||
attachment.getWidth(),
|
||||
attachment.getHeight(),
|
||||
attachment.getSize(),
|
||||
Optional.absent(),
|
||||
Optional.fromNullable(attachment.getCaption())));
|
||||
for (Attachment attachment : attachments) {
|
||||
Uri uri = attachment.getDataUri() != null ? attachment.getDataUri() : attachment.getThumbnailUri();
|
||||
|
||||
if (uri != null) {
|
||||
mediaList.add(new Media(uri,
|
||||
attachment.getContentType(),
|
||||
System.currentTimeMillis(),
|
||||
attachment.getWidth(),
|
||||
attachment.getHeight(),
|
||||
attachment.getSize(),
|
||||
Optional.absent(),
|
||||
Optional.fromNullable(attachment.getCaption())));
|
||||
}
|
||||
};
|
||||
|
||||
if (!mediaList.isEmpty()) {
|
||||
composeIntent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaList);
|
||||
}
|
||||
} else if (mediaMessage.containsMediaSlide()) {
|
||||
Slide slide = mediaMessage.getSlideDeck().getSlides().get(0);
|
||||
composeIntent.putExtra(Intent.EXTRA_STREAM, slide.getUri());
|
||||
composeIntent.setType(slide.getContentType());
|
||||
}
|
||||
|
||||
if (mediaMessage.getSlideDeck().getTextSlide() != null && mediaMessage.getSlideDeck().getTextSlide().getUri() != null) {
|
||||
try (InputStream stream = PartAuthority.getAttachmentStream(requireContext(), mediaMessage.getSlideDeck().getTextSlide().getUri())) {
|
||||
String extraText = Util.readFullyAsString(stream);
|
||||
composeIntent.putExtra(Intent.EXTRA_TEXT, message.getDisplayBody().toString() + extraText);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to read long message text when forwarding.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!mediaList.isEmpty()) {
|
||||
composeIntent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaList);
|
||||
}
|
||||
} else if (mediaMessage.containsMediaSlide()) {
|
||||
Slide slide = mediaMessage.getSlideDeck().getSlides().get(0);
|
||||
composeIntent.putExtra(Intent.EXTRA_STREAM, slide.getUri());
|
||||
composeIntent.setType(slide.getContentType());
|
||||
}
|
||||
}
|
||||
startActivity(composeIntent);
|
||||
|
||||
return composeIntent;
|
||||
}, this::startActivity);
|
||||
}
|
||||
|
||||
private void handleResendMessage(final MessageRecord message) {
|
||||
@ -910,6 +931,13 @@ public class ConversationFragment extends Fragment
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMoreTextClicked(@NonNull Address conversationAddress, long messageId, boolean isMms) {
|
||||
if (getContext() != null && getActivity() != null) {
|
||||
startActivity(LongMessageActivity.getIntent(getContext(), conversationAddress, messageId, isMms));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView) {
|
||||
if (getContext() != null && getActivity() != null) {
|
||||
|
@ -23,6 +23,7 @@ import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.DimenRes;
|
||||
import android.support.annotation.NonNull;
|
||||
@ -30,9 +31,13 @@ import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
@ -46,6 +51,8 @@ import org.thoughtcrime.securesms.MessageDetailsActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.LinkPreviewView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@ -87,6 +94,7 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.mms.TextSlide;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
@ -119,7 +127,8 @@ public class ConversationItem extends LinearLayout
|
||||
{
|
||||
private static final String TAG = ConversationItem.class.getSimpleName();
|
||||
|
||||
private static final int MAX_MEASURE_CALLS = 3;
|
||||
private static final int MAX_MEASURE_CALLS = 3;
|
||||
private static final int MAX_BODY_DISPLAY_LENGTH = 1000;
|
||||
|
||||
private MessageRecord messageRecord;
|
||||
private Locale locale;
|
||||
@ -129,7 +138,7 @@ public class ConversationItem extends LinearLayout
|
||||
|
||||
protected ViewGroup bodyBubble;
|
||||
private QuoteView quoteView;
|
||||
private TextView bodyText;
|
||||
private EmojiTextView bodyText;
|
||||
private ConversationItemFooter footer;
|
||||
private TextView groupSender;
|
||||
private TextView groupSenderProfileName;
|
||||
@ -378,7 +387,7 @@ public class ConversationItem extends LinearLayout
|
||||
}
|
||||
|
||||
private boolean isCaptionlessMms(MessageRecord messageRecord) {
|
||||
return TextUtils.isEmpty(messageRecord.getDisplayBody()) && messageRecord.isMms();
|
||||
return TextUtils.isEmpty(messageRecord.getDisplayBody()) && messageRecord.isMms() && ((MmsMessageRecord) messageRecord).getSlideDeck().getTextSlide() == null;
|
||||
}
|
||||
|
||||
private boolean hasAudio(MessageRecord messageRecord) {
|
||||
@ -397,6 +406,13 @@ public class ConversationItem extends LinearLayout
|
||||
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null;
|
||||
}
|
||||
|
||||
private boolean hasExtraText(MessageRecord messageRecord) {
|
||||
boolean hasTextSlide = messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getTextSlide() != null;
|
||||
boolean hasOverflowText = messageRecord.getBody().length() > MAX_BODY_DISPLAY_LENGTH;
|
||||
|
||||
return hasTextSlide || hasOverflowText;
|
||||
}
|
||||
|
||||
private boolean hasQuote(MessageRecord messageRecord) {
|
||||
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getQuote() != null;
|
||||
}
|
||||
@ -421,6 +437,12 @@ public class ConversationItem extends LinearLayout
|
||||
styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery);
|
||||
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery);
|
||||
|
||||
if (hasExtraText(messageRecord)) {
|
||||
bodyText.setOverflowText(getLongMessageSpan(messageRecord));
|
||||
} else {
|
||||
bodyText.setOverflowText(null);
|
||||
}
|
||||
|
||||
bodyText.setText(styledText);
|
||||
bodyText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
@ -536,7 +558,7 @@ public class ConversationItem extends LinearLayout
|
||||
mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener);
|
||||
mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
mediaThumbnailStub.get().setOnClickListener(passthroughClickListener);
|
||||
mediaThumbnailStub.get().showShade(TextUtils.isEmpty(messageRecord.getDisplayBody()));
|
||||
mediaThumbnailStub.get().showShade(TextUtils.isEmpty(messageRecord.getDisplayBody()) && !hasExtraText(messageRecord));
|
||||
mediaThumbnailStub.get().setConversationColor(messageRecord.isOutgoing() ? defaultBubbleColor
|
||||
: messageRecord.getRecipient().getColor().toConversationColor(context));
|
||||
|
||||
@ -613,7 +635,7 @@ public class ConversationItem extends LinearLayout
|
||||
topRight = 0;
|
||||
}
|
||||
|
||||
if (hasLinkPreview(messageRecord)) {
|
||||
if (hasLinkPreview(messageRecord) || hasExtraText(messageRecord)) {
|
||||
bottomLeft = 0;
|
||||
bottomRight = 0;
|
||||
}
|
||||
@ -748,7 +770,7 @@ public class ConversationItem extends LinearLayout
|
||||
ViewUtil.updateLayoutParams(footer, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
|
||||
footer.setVisibility(GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().getFooter().setVisibility(GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().getFooter().setVisibility(GONE);
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().getFooter().setVisibility(GONE);
|
||||
|
||||
boolean differentTimestamps = next.isPresent() && !DateUtils.isSameExtendedRelativeTimestamp(context, locale, next.get().getTimestamp(), current.getTimestamp());
|
||||
@ -899,6 +921,49 @@ public class ConversationItem extends LinearLayout
|
||||
new ConfirmIdentityDialog(context, messageRecord, mismatches.get(0)).show();
|
||||
}
|
||||
|
||||
private Spannable getLongMessageSpan(@NonNull MessageRecord messageRecord) {
|
||||
String message;
|
||||
Runnable action;
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
TextSlide slide = ((MmsMessageRecord) messageRecord).getSlideDeck().getTextSlide();
|
||||
|
||||
if (slide != null && slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
|
||||
message = getResources().getString(R.string.ConversationItem_read_more);
|
||||
action = () -> eventListener.onMoreTextClicked(conversationRecipient.getAddress(), messageRecord.getId(), messageRecord.isMms());
|
||||
} else if (slide != null && slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
|
||||
message = getResources().getString(R.string.ConversationItem_pending);
|
||||
action = () -> {};
|
||||
} else if (slide != null) {
|
||||
message = getResources().getString(R.string.ConversationItem_download_more);
|
||||
action = () -> singleDownloadClickListener.onClick(bodyText, slide);
|
||||
} else {
|
||||
message = getResources().getString(R.string.ConversationItem_read_more);
|
||||
action = () -> eventListener.onMoreTextClicked(conversationRecipient.getAddress(), messageRecord.getId(), messageRecord.isMms());
|
||||
}
|
||||
} else {
|
||||
message = getResources().getString(R.string.ConversationItem_read_more);
|
||||
action = () -> eventListener.onMoreTextClicked(conversationRecipient.getAddress(), messageRecord.getId(), messageRecord.isMms());
|
||||
}
|
||||
|
||||
SpannableStringBuilder span = new SpannableStringBuilder(message);
|
||||
CharacterStyle style = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
if (eventListener != null && batchSelected.isEmpty()) {
|
||||
action.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint ds) {
|
||||
ds.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
}
|
||||
};
|
||||
span.setSpan(style, 0, span.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
return span;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModified(final Recipient modified) {
|
||||
Util.runOnMain(() -> {
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
|
26
src/org/thoughtcrime/securesms/longmessage/LongMessage.java
Normal file
26
src/org/thoughtcrime/securesms/longmessage/LongMessage.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
22
src/org/thoughtcrime/securesms/mms/TextSlide.java
Normal file
22
src/org/thoughtcrime/securesms/mms/TextSlide.java
Normal 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));
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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));
|
||||
|
@ -45,14 +45,16 @@ public abstract class CharacterCalculator {
|
||||
}
|
||||
|
||||
public static class CharacterState {
|
||||
public int charactersRemaining;
|
||||
public int messagesSpent;
|
||||
public int maxMessageSize;
|
||||
public final int charactersRemaining;
|
||||
public final int messagesSpent;
|
||||
public final int maxTotalMessageSize;
|
||||
public final int maxPrimaryMessageSize;
|
||||
|
||||
public CharacterState(int messagesSpent, int charactersRemaining, int maxMessageSize) {
|
||||
this.messagesSpent = messagesSpent;
|
||||
this.charactersRemaining = charactersRemaining;
|
||||
this.maxMessageSize = maxMessageSize;
|
||||
public CharacterState(int messagesSpent, int charactersRemaining, int maxTotalMessageSize, int maxPrimaryMessageSize) {
|
||||
this.messagesSpent = messagesSpent;
|
||||
this.charactersRemaining = charactersRemaining;
|
||||
this.maxTotalMessageSize = maxTotalMessageSize;
|
||||
this.maxPrimaryMessageSize = maxPrimaryMessageSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user