diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b06617ec3d..a6cc1bd48f 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -299,7 +299,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -336,6 +336,13 @@
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+
+
+
+
+
+
diff --git a/res/drawable/ic_play_outline_24.xml b/res/drawable/ic_play_outline_24.xml
new file mode 100644
index 0000000000..c9b8c5f0b5
--- /dev/null
+++ b/res/drawable/ic_play_outline_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/res/drawable/ic_play_solid_24.xml b/res/drawable/ic_play_solid_24.xml
new file mode 100644
index 0000000000..ec9e2ba5b0
--- /dev/null
+++ b/res/drawable/ic_play_solid_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/res/drawable/ic_timer_24.xml b/res/drawable/ic_timer_24.xml
new file mode 100644
index 0000000000..a96076ca3c
--- /dev/null
+++ b/res/drawable/ic_timer_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/res/drawable/ic_timer_disabled_24.xml b/res/drawable/ic_timer_disabled_24.xml
new file mode 100644
index 0000000000..f232761ec5
--- /dev/null
+++ b/res/drawable/ic_timer_disabled_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/res/layout/conversation_item_received.xml b/res/layout/conversation_item_received.xml
index 82451b14d6..b9adf27b26 100644
--- a/res/layout/conversation_item_received.xml
+++ b/res/layout/conversation_item_received.xml
@@ -153,6 +153,16 @@
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding" />
+
+
+
diff --git a/res/layout/conversation_item_sent.xml b/res/layout/conversation_item_sent.xml
index 48fc73b18a..bda67aecfc 100644
--- a/res/layout/conversation_item_sent.xml
+++ b/res/layout/conversation_item_sent.xml
@@ -94,6 +94,16 @@
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding" />
+
+
+
diff --git a/res/layout/mediasend_activity.xml b/res/layout/mediasend_activity.xml
index 6caab7a11d..c404cd9141 100644
--- a/res/layout/mediasend_activity.xml
+++ b/res/layout/mediasend_activity.xml
@@ -60,6 +60,15 @@
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
android:orientation="horizontal">
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/revealable_message_view.xml b/res/layout/revealable_message_view.xml
new file mode 100644
index 0000000000..c300c25c1d
--- /dev/null
+++ b/res/layout/revealable_message_view.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 08ef25fb1c..2754046368 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -94,6 +94,7 @@
+
@@ -347,4 +348,9 @@
+
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1f033ab244..fd9a2b082f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -639,6 +639,11 @@
Enter the code we sent to %s
Call
+
+ View Photo
+ Viewed
+ Photo
+
Failed to save image changes
@@ -677,6 +682,7 @@
%s reset the secure session.
Duplicate message.
This message could not be processed because it was sent from a newer version of Signal. You can ask your contact to send this message again after you update.
+ Error handling incoming message
Stickers
@@ -707,6 +713,8 @@
Called you
Missed call
Media message
+ Sticker
+ Disappearing photo
%s is on Signal!
Disappearing messages disabled
Disappearing message time set to %s
@@ -794,6 +802,7 @@
Mark read
Media message
Sticker
+ Disappearing photo
Reply
Signal Message
Unsecured SMS
diff --git a/res/values/themes.xml b/res/values/themes.xml
index 4be64a9916..fa31a40658 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -219,6 +219,7 @@
- @drawable/sticky_date_header_background_light
- @color/core_grey_60
- @color/transparent_black_30
+ - @color/core_white
- @drawable/quick_camera_light
- @drawable/ic_mic_grey600_24dp
@@ -355,6 +356,7 @@
- @drawable/sticky_date_header_background_dark
- @color/core_grey_25
- @color/transparent_white_30
+ - @color/core_black
- @drawable/contact_list_divider_dark
@@ -495,4 +497,8 @@
+
+
diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java
index 94158d3817..7d166bdb84 100644
--- a/src/org/thoughtcrime/securesms/ApplicationContext.java
+++ b/src/org/thoughtcrime/securesms/ApplicationContext.java
@@ -62,6 +62,7 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.LocalBackupListener;
+import org.thoughtcrime.securesms.revealable.RevealableMessageManager;
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
@@ -90,12 +91,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
private static final String TAG = ApplicationContext.class.getSimpleName();
- private ExpiringMessageManager expiringMessageManager;
- private TypingStatusRepository typingStatusRepository;
- private TypingStatusSender typingStatusSender;
- private JobManager jobManager;
- private IncomingMessageObserver incomingMessageObserver;
- private PersistentLogger persistentLogger;
+ private ExpiringMessageManager expiringMessageManager;
+ private RevealableMessageManager revealableMessageManager;
+ private TypingStatusRepository typingStatusRepository;
+ private TypingStatusSender typingStatusSender;
+ private JobManager jobManager;
+ private IncomingMessageObserver incomingMessageObserver;
+ private PersistentLogger persistentLogger;
private volatile boolean isAppVisible;
@@ -114,6 +116,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
initializeJobManager();
initializeMessageRetrieval();
initializeExpiringMessageManager();
+ initializeRevealableMessageManager();
initializeTypingStatusRepository();
initializeTypingStatusSender();
initializeGcmCheck();
@@ -154,6 +157,10 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
return expiringMessageManager;
}
+ public RevealableMessageManager getRevealableMessageManager() {
+ return revealableMessageManager;
+ }
+
public TypingStatusRepository getTypingStatusRepository() {
return typingStatusRepository;
}
@@ -244,6 +251,10 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
this.expiringMessageManager = new ExpiringMessageManager(this);
}
+ private void initializeRevealableMessageManager() {
+ this.revealableMessageManager = new RevealableMessageManager(this);
+ }
+
private void initializeTypingStatusRepository() {
this.typingStatusRepository = new TypingStatusRepository();
}
diff --git a/src/org/thoughtcrime/securesms/BindableConversationItem.java b/src/org/thoughtcrime/securesms/BindableConversationItem.java
index ae11885fd7..fdded7b42c 100644
--- a/src/org/thoughtcrime/securesms/BindableConversationItem.java
+++ b/src/org/thoughtcrime/securesms/BindableConversationItem.java
@@ -38,6 +38,7 @@ public interface BindableConversationItem extends Unbindable {
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
void onMoreTextClicked(@NonNull Address conversationAddress, long messageId, boolean isMms);
void onStickerClicked(@NonNull StickerLocator stickerLocator);
+ void onRevealableMessageClicked(@NonNull MmsMessageRecord messageRecord);
void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView);
void onAddToContactsClicked(@NonNull Contact contact);
void onMessageSharedContactClicked(@NonNull List choices);
diff --git a/src/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java b/src/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java
new file mode 100644
index 0000000000..79feb548c3
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java
@@ -0,0 +1,25 @@
+package org.thoughtcrime.securesms.attachments;
+
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.thoughtcrime.securesms.database.AttachmentDatabase;
+
+public class TombstoneAttachment extends Attachment {
+
+ public TombstoneAttachment(@NonNull String contentType, boolean quote) {
+ super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, null, null, null, null, null, false, 0, 0, quote, null, null);
+ }
+
+ @Override
+ public @Nullable Uri getDataUri() {
+ return null;
+ }
+
+ @Override
+ public @Nullable Uri getThumbnailUri() {
+ return null;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java b/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java
index bd0864f065..c698a5dfd3 100644
--- a/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java
+++ b/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java
@@ -75,8 +75,8 @@ public class FullBackupExporter extends FullBackupBase {
int count = 0;
for (String table : tables) {
- if (table.equals(SmsDatabase.TABLE_NAME) || table.equals(MmsDatabase.TABLE_NAME)) {
- count = exportTable(table, input, outputStream, cursor -> cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0, null, count);
+ if (table.equals(MmsDatabase.TABLE_NAME)) {
+ count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMessage, null, count);
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count);
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
@@ -253,14 +253,20 @@ public class FullBackupExporter extends FullBackupBase {
return result;
}
+ private static boolean isNonExpiringMessage(@NonNull Cursor cursor) {
+ return cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0 &&
+ cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.REVEAL_DURATION)) <= 0;
+ }
+
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long mmsId) {
- String[] columns = new String[] { MmsDatabase.EXPIRES_IN };
+ String[] columns = new String[] { MmsDatabase.EXPIRES_IN, MmsDatabase.REVEAL_DURATION };
String where = MmsDatabase.ID + " = ?";
String[] args = new String[] { String.valueOf(mmsId) };
try (Cursor mmsCursor = db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null)) {
if (mmsCursor != null && mmsCursor.moveToFirst()) {
- return mmsCursor.getLong(0) == 0;
+ return mmsCursor.getLong(mmsCursor.getColumnIndexOrThrow(MmsDatabase.EXPIRES_IN)) == 0 &&
+ mmsCursor.getLong(mmsCursor.getColumnIndexOrThrow(MmsDatabase.REVEAL_DURATION)) == 0;
}
}
diff --git a/src/org/thoughtcrime/securesms/components/Outliner.java b/src/org/thoughtcrime/securesms/components/Outliner.java
index 64a4bfbb86..1702612852 100644
--- a/src/org/thoughtcrime/securesms/components/Outliner.java
+++ b/src/org/thoughtcrime/securesms/components/Outliner.java
@@ -25,12 +25,16 @@ public class Outliner {
}
public void draw(Canvas canvas) {
+ draw(canvas, 0, canvas.getWidth(), canvas.getHeight(), 0);
+ }
+
+ public void draw(Canvas canvas, int top, int right, int bottom, int left) {
final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2;
- bounds.left = halfStrokeWidth;
- bounds.top = halfStrokeWidth;
- bounds.right = canvas.getWidth() - halfStrokeWidth;
- bounds.bottom = canvas.getHeight() - halfStrokeWidth;
+ bounds.left = left + halfStrokeWidth;
+ bounds.top = top + halfStrokeWidth;
+ bounds.right = right - halfStrokeWidth;
+ bounds.bottom = bottom - halfStrokeWidth;
corners.reset();
corners.addRoundRect(bounds, radii, Path.Direction.CW);
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index d55fc20db0..0c8d4486eb 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -92,6 +92,8 @@ import org.thoughtcrime.securesms.RegistrationActivity;
import org.thoughtcrime.securesms.ShortcutLauncherActivity;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.VerifyIdentityActivity;
+import org.thoughtcrime.securesms.attachments.Attachment;
+import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
import org.thoughtcrime.securesms.audio.AudioRecorder;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.color.MaterialColor;
@@ -539,6 +541,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
boolean initiating = threadId == -1;
TransportOption transport = data.getParcelableExtra(MediaSendActivity.EXTRA_TRANSPORT);
String message = data.getStringExtra(MediaSendActivity.EXTRA_MESSAGE);
+ long revealDuration = data.getLongExtra(MediaSendActivity.EXTRA_REVEAL_DURATION, 0);
+ QuoteModel quote = (revealDuration == 0) ? inputPanel.getQuote().orNull() : null;
SlideDeck slideDeck = new SlideDeck();
if (transport == null) {
@@ -566,10 +570,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
sendMediaMessage(transport.isSms(),
message,
slideDeck,
- inputPanel.getQuote().orNull(),
+ quote,
Collections.emptyList(),
Collections.emptyList(),
expiresIn,
+ revealDuration,
subscriptionId,
initiating,
true).addListener(new AssertedSuccessListener() {
@@ -1807,7 +1812,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
long expiresIn = recipient.getExpireMessages() * 1000L;
boolean initiating = threadId == -1;
- sendMediaMessage(isSmsForced(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), expiresIn, subscriptionId, initiating, false);
+ sendMediaMessage(isSmsForced(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), expiresIn, 0, subscriptionId, initiating, false);
}
private void selectContactInfo(ContactData contactData) {
@@ -2129,7 +2134,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} else if (!forceSms && identityRecords.isUntrusted()) {
handleUntrustedRecipients();
} else if (isMediaMessage) {
- sendMediaMessage(forceSms, expiresIn, subscriptionId, initiating);
+ sendMediaMessage(forceSms, expiresIn, 0, subscriptionId, initiating);
} else {
sendTextMessage(forceSms, expiresIn, subscriptionId, initiating);
}
@@ -2145,11 +2150,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
- private void sendMediaMessage(final boolean forceSms, final long expiresIn, final int subscriptionId, boolean initiating)
+ private void sendMediaMessage(final boolean forceSms, final long expiresIn, final long revealDuration, final int subscriptionId, boolean initiating)
throws InvalidMessageException
{
Log.i(TAG, "Sending media message...");
- sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), inputPanel.getQuote().orNull(), Collections.emptyList(), linkPreviewViewModel.getActiveLinkPreviews(), expiresIn, subscriptionId, initiating, true);
+ sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), inputPanel.getQuote().orNull(), Collections.emptyList(), linkPreviewViewModel.getActiveLinkPreviews(), expiresIn, revealDuration, subscriptionId, initiating, true);
}
private ListenableFuture sendMediaMessage(final boolean forceSms,
@@ -2159,6 +2164,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
List contacts,
List previews,
final long expiresIn,
+ final long revealDuration,
final int subscriptionId,
final boolean initiating,
final boolean clearComposeBox)
@@ -2177,7 +2183,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
- OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType, quote, contacts, previews);
+ OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, revealDuration, distributionType, quote, contacts, previews);
final SettableFuture future = new SettableFuture<>();
final Context context = getApplicationContext();
@@ -2378,7 +2384,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
SlideDeck slideDeck = new SlideDeck();
slideDeck.addSlide(audioSlide);
- sendMediaMessage(forceSms, "", slideDeck, inputPanel.getQuote().orNull(), Collections.emptyList(), Collections.emptyList(), expiresIn, subscriptionId, initiating, true).addListener(new AssertedSuccessListener() {
+ sendMediaMessage(forceSms, "", slideDeck, inputPanel.getQuote().orNull(), Collections.emptyList(), Collections.emptyList(), expiresIn, 0, subscriptionId, initiating, true).addListener(new AssertedSuccessListener() {
@Override
public void onSuccess(Void nothing) {
new AsyncTask() {
@@ -2506,7 +2512,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
slideDeck.addSlide(stickerSlide);
- sendMediaMessage(transport.isSms(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), expiresIn, subscriptionId, initiating, clearCompose);
+ sendMediaMessage(transport.isSms(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), expiresIn, 0, subscriptionId, initiating, clearCompose);
}
@@ -2687,11 +2693,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
messageRecord.getBody(),
slideDeck);
} else {
+ SlideDeck slideDeck = messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck();
+
+ if (messageRecord.isMms() && ((MmsMessageRecord) messageRecord).getRevealDuration() > 0 && slideDeck.getSlides().size() > 0) {
+ Attachment attachment = new TombstoneAttachment(slideDeck.getSlides().get(0).getContentType(), true);
+ slideDeck = new SlideDeck();
+ slideDeck.addSlide(MediaUtil.getSlideForAttachment(this, attachment));
+ }
+
inputPanel.setQuote(GlideApp.with(this),
messageRecord.getDateSent(),
author,
messageRecord.getBody(),
- messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck());
+ slideDeck);
}
}
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
index db04cbfae1..4ed11fed3c 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -71,6 +71,7 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHol
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.MessagingDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
@@ -78,6 +79,7 @@ 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.jobs.MultiDeviceRevealUpdateJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
@@ -88,6 +90,8 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.profiles.UnknownSenderView;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.revealable.RevealableMessageActivity;
+import org.thoughtcrime.securesms.revealable.RevealableUtil;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.stickers.StickerLocator;
@@ -958,6 +962,38 @@ public class ConversationFragment extends Fragment
}
}
+ @Override
+ public void onRevealableMessageClicked(@NonNull MmsMessageRecord messageRecord) {
+ if (messageRecord.getRevealDuration() == 0) {
+ throw new AssertionError("Non-revealable message clicked.");
+ }
+
+ if (messageRecord.getRevealStartTime() == 0) {
+ SimpleTask.run(getLifecycle(), () -> {
+ if (!messageRecord.isOutgoing()) {
+ Log.i(TAG, "Marking revealable message as opened.");
+
+ DatabaseFactory.getMmsDatabase(requireContext()).markRevealStarted(messageRecord.getId());
+
+ ApplicationContext.getInstance(requireContext())
+ .getRevealableMessageManager()
+ .scheduleIfNecessary();
+
+ ApplicationContext.getInstance(requireContext())
+ .getJobManager()
+ .add(new MultiDeviceRevealUpdateJob(new MessagingDatabase.SyncMessageId(messageRecord.getIndividualRecipient().getAddress(), messageRecord.getDateSent())));
+ } else {
+ Log.i(TAG, "Opening your own revealable message. It will automatically be marked as opened when it is sent.");
+ }
+ return null;
+ }, (nothing) -> {
+ startActivity(RevealableMessageActivity.getIntent(requireContext(), messageRecord.getId()));
+ });
+ } else if (RevealableUtil.isViewable(messageRecord)) {
+ startActivity(RevealableMessageActivity.getIntent(requireContext(), messageRecord.getId()));
+ }
+ }
+
@Override
public void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView) {
if (getContext() != null && getActivity() != null) {
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java
index 141ca4be31..ee8b44c974 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java
@@ -21,6 +21,7 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
+import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Typeface;
@@ -65,7 +66,9 @@ import org.thoughtcrime.securesms.components.ConversationItemFooter;
import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.LinkPreviewView;
+import org.thoughtcrime.securesms.components.Outliner;
import org.thoughtcrime.securesms.components.QuoteView;
+import org.thoughtcrime.securesms.revealable.RevealableMessageView;
import org.thoughtcrime.securesms.components.SharedContactView;
import org.thoughtcrime.securesms.components.StickerView;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
@@ -96,6 +99,7 @@ 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.revealable.RevealableUtil;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicTheme;
@@ -151,6 +155,7 @@ public class ConversationItem extends LinearLayout
private ViewGroup container;
private @NonNull Set batchSelected = new HashSet<>();
+ private @NonNull Outliner outliner = new Outliner();
private Recipient conversationRecipient;
private Stub mediaThumbnailStub;
private Stub audioViewStub;
@@ -158,6 +163,7 @@ public class ConversationItem extends LinearLayout
private Stub sharedContactStub;
private Stub linkPreviewStub;
private Stub stickerStub;
+ private Stub revealableStub;
private @Nullable EventListener eventListener;
private int defaultBubbleColor;
@@ -169,6 +175,7 @@ public class ConversationItem extends LinearLayout
private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener();
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener();
+ private final RevealableMessageClickListener revealableClickListener = new RevealableMessageClickListener();
private final Context context;
@@ -207,6 +214,7 @@ public class ConversationItem extends LinearLayout
this.sharedContactStub = new Stub<>(findViewById(R.id.shared_contact_view_stub));
this.linkPreviewStub = new Stub<>(findViewById(R.id.link_preview_stub));
this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub));
+ this.revealableStub = new Stub<>(findViewById(R.id.revealable_view_stub));
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
this.quoteView = findViewById(R.id.quote_view);
this.container = findViewById(R.id.container);
@@ -302,11 +310,21 @@ public class ConversationItem extends LinearLayout
}
}
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+
+ if (!messageRecord.isOutgoing() && hasRevealableMessage(messageRecord) && RevealableUtil.isRevealExpired((MmsMessageRecord) messageRecord)) {
+ outliner.setColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_text_secondary_color));
+ outliner.draw(canvas, bodyBubble.getTop() + getPaddingTop(), bodyBubble.getRight(), bodyBubble.getBottom() + getPaddingTop(), bodyBubble.getLeft());
+ }
+ }
+
private int getAvailableMessageBubbleWidth(@NonNull View forView) {
int availableWidth;
if (hasAudio(messageRecord)) {
availableWidth = audioViewStub.get().getMeasuredWidth() + ViewUtil.getLeftMargin(audioViewStub.get()) + ViewUtil.getRightMargin(audioViewStub.get());
- } else if (hasThumbnail(messageRecord) || hasBigImageLinkPreview(messageRecord)) {
+ } else if (!hasRevealableMessage(messageRecord) && (hasThumbnail(messageRecord) || hasBigImageLinkPreview(messageRecord))) {
availableWidth = mediaThumbnailStub.get().getMeasuredWidth();
} else {
availableWidth = bodyBubble.getMeasuredWidth() - bodyBubble.getPaddingLeft() - bodyBubble.getPaddingRight();
@@ -341,8 +359,16 @@ public class ConversationItem extends LinearLayout
private void setBubbleState(MessageRecord messageRecord) {
if (messageRecord.isOutgoing()) {
bodyBubble.getBackground().setColorFilter(defaultBubbleColor, PorterDuff.Mode.MULTIPLY);
+ footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_text_secondary_color));
+ footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_icon_color));
+ } else if (hasRevealableMessage(messageRecord) && RevealableUtil.isRevealExpired((MmsMessageRecord) messageRecord)) {
+ bodyBubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(context, R.attr.conversation_item_reveal_viewed_background_color), PorterDuff.Mode.MULTIPLY);
+ footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_text_secondary_color));
+ footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_icon_color));
} else {
bodyBubble.getBackground().setColorFilter(messageRecord.getRecipient().getColor().toConversationColor(context), PorterDuff.Mode.MULTIPLY);
+ footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_secondary_color));
+ footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_secondary_color));
}
if (audioViewStub.resolved()) {
@@ -413,7 +439,8 @@ public class ConversationItem extends LinearLayout
!hasAudio(messageRecord) &&
!hasDocument(messageRecord) &&
!hasSharedContact(messageRecord) &&
- !hasSticker(messageRecord);
+ !hasSticker(messageRecord) &&
+ !hasRevealableMessage(messageRecord);
}
private boolean hasDocument(MessageRecord messageRecord) {
@@ -450,6 +477,10 @@ public class ConversationItem extends LinearLayout
!StickerUrl.isValidShareLink(linkPreview.getUrl());
}
+ private boolean hasRevealableMessage(MessageRecord messageRecord) {
+ return messageRecord.isMms() && ((MmsMessageRecord) messageRecord).getRevealDuration() > 0;
+ }
+
private void setBodyText(MessageRecord messageRecord, @Nullable String searchQuery) {
bodyText.setClickable(false);
bodyText.setFocusable(false);
@@ -481,13 +512,28 @@ public class ConversationItem extends LinearLayout
{
boolean showControls = !messageRecord.isFailed();
- if (hasSharedContact(messageRecord)) {
+ if (hasRevealableMessage(messageRecord)) {
+ revealableStub.get().setVisibility(VISIBLE);
+ if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
+ if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
+ if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
+ if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
+ if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
+ if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
+
+ revealableStub.get().setMessage((MmsMessageRecord) messageRecord);
+ revealableStub.get().setOnClickListener(revealableClickListener);
+ revealableStub.get().setOnLongClickListener(passthroughClickListener);
+
+ footer.setVisibility(VISIBLE);
+ } else if (hasSharedContact(messageRecord)) {
sharedContactStub.get().setVisibility(VISIBLE);
if (audioViewStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
+ if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
sharedContactStub.get().setContact(((MediaMmsMessageRecord) messageRecord).getSharedContacts().get(0), glideRequests, locale);
sharedContactStub.get().setEventListener(sharedContactEventListener);
@@ -506,6 +552,7 @@ public class ConversationItem extends LinearLayout
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
+ if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
@@ -544,6 +591,7 @@ public class ConversationItem extends LinearLayout
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
+ if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
audioViewStub.get().setAudio(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls);
@@ -561,6 +609,7 @@ public class ConversationItem extends LinearLayout
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
+ if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
documentViewStub.get().setDocument(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getDocumentSlide(), showControls);
@@ -581,6 +630,7 @@ public class ConversationItem extends LinearLayout
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
+ if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
stickerStub.get().setSticker(glideRequests, ((MmsMessageRecord) messageRecord).getSlideDeck().getStickerSlide());
@@ -600,6 +650,7 @@ public class ConversationItem extends LinearLayout
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
+ if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
List thumbnailSlides = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlides();
@@ -629,6 +680,7 @@ public class ConversationItem extends LinearLayout
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
+ if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
@@ -876,7 +928,10 @@ public class ConversationItem extends LinearLayout
}
private void setGroupAuthorColor(@NonNull MessageRecord messageRecord) {
- if (hasSticker(messageRecord)) {
+ if (!messageRecord.isOutgoing() && hasRevealableMessage(messageRecord) && RevealableUtil.isRevealExpired((MmsMessageRecord) messageRecord)) {
+ groupSender.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_sticker_author_color));
+ groupSenderProfileName.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_sticker_author_color));
+ } else if (hasSticker(messageRecord)) {
groupSender.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_sticker_author_color));
groupSenderProfileName.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_sticker_author_color));
} else {
@@ -912,19 +967,43 @@ public class ConversationItem extends LinearLayout
}
private void setMessageShape(@NonNull MessageRecord current, @NonNull Optional previous, @NonNull Optional next, boolean isGroupThread) {
+ int bigRadius = readDimen(R.dimen.message_corner_radius);
+ int smallRadius = readDimen(R.dimen.message_corner_collapse_radius);
+
int background;
+
if (isSingularMessage(current, previous, next, isGroupThread)) {
- background = current.isOutgoing() ? R.drawable.message_bubble_background_sent_alone
- : R.drawable.message_bubble_background_received_alone;
+ if (current.isOutgoing()) {
+ background = R.drawable.message_bubble_background_sent_alone;
+ outliner.setRadius(bigRadius);
+ } else {
+ background = R.drawable.message_bubble_background_received_alone;
+ outliner.setRadius(bigRadius);
+ }
} else if (isStartOfMessageCluster(current, previous, isGroupThread)) {
- background = current.isOutgoing() ? R.drawable.message_bubble_background_sent_start
- : R.drawable.message_bubble_background_received_start;
+ if (current.isOutgoing()) {
+ background = R.drawable.message_bubble_background_sent_start;
+ outliner.setRadii(bigRadius, bigRadius, smallRadius, bigRadius);
+ } else {
+ background = R.drawable.message_bubble_background_received_start;
+ outliner.setRadii(bigRadius, bigRadius, bigRadius, smallRadius);
+ }
} else if (isEndOfMessageCluster(current, next, isGroupThread)) {
- background = current.isOutgoing() ? R.drawable.message_bubble_background_sent_end
- : R.drawable.message_bubble_background_received_end;
+ if (current.isOutgoing()) {
+ background = R.drawable.message_bubble_background_sent_end;
+ outliner.setRadii(bigRadius, smallRadius, bigRadius, bigRadius);
+ } else {
+ background = R.drawable.message_bubble_background_received_end;
+ outliner.setRadii(smallRadius, bigRadius, bigRadius, bigRadius);
+ }
} else {
- background = current.isOutgoing() ? R.drawable.message_bubble_background_sent_middle
- : R.drawable.message_bubble_background_received_middle;
+ if (current.isOutgoing()) {
+ background = R.drawable.message_bubble_background_sent_middle;
+ outliner.setRadii(bigRadius, smallRadius, smallRadius, bigRadius);
+ } else {
+ background = R.drawable.message_bubble_background_received_middle;
+ outliner.setRadii(smallRadius, bigRadius, bigRadius, smallRadius);
+ }
}
bodyBubble.setBackgroundResource(background);
@@ -1090,6 +1169,21 @@ public class ConversationItem extends LinearLayout
}
}
+ private class RevealableMessageClickListener implements View.OnClickListener {
+ @Override
+ public void onClick(View view) {
+ RevealableMessageView revealView = (RevealableMessageView) view;
+
+ if (eventListener != null && batchSelected.isEmpty() && messageRecord.isMms() && RevealableUtil.isViewable((MmsMessageRecord) messageRecord)) {
+ eventListener.onRevealableMessageClicked((MmsMessageRecord) messageRecord);
+ } else if (batchSelected.isEmpty() && messageRecord.isMms() && revealView.requiresTapToDownload((MmsMessageRecord) messageRecord)) {
+ singleDownloadClickListener.onClick(view, ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlide());
+ } else {
+ passthroughClickListener.onClick(view);
+ }
+ }
+ }
+
private class LinkPreviewThumbnailClickListener implements SlideClickListener {
public void onClick(final View v, final Slide slide) {
if (eventListener != null && batchSelected.isEmpty() && messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) {
diff --git a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
index d5e46f6888..f3e6995f0e 100644
--- a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
@@ -245,6 +245,15 @@ public class AttachmentDatabase extends Database {
}
}
+ public boolean hasAttachmentFilesForMessage(long mmsId) {
+ String selection = MMS_ID + " = ? AND (" + DATA + " NOT NULL OR " + TRANSFER_STATE + " != ?)";
+ String[] args = new String[] { String.valueOf(mmsId), String.valueOf(TRANSFER_PROGRESS_DONE) };
+
+ try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, "1")) {
+ return cursor != null && cursor.moveToFirst();
+ }
+ }
+
public @NonNull List getPendingAttachments() {
final SQLiteDatabase database = databaseHelper.getReadableDatabase();
final List attachments = new LinkedList<>();
@@ -263,7 +272,7 @@ public class AttachmentDatabase extends Database {
}
@SuppressWarnings("ResultOfMethodCallIgnored")
- void deleteAttachmentsForMessage(long mmsId) {
+ public void deleteAttachmentsForMessage(long mmsId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Cursor cursor = null;
@@ -283,6 +292,44 @@ public class AttachmentDatabase extends Database {
notifyAttachmentListeners();
}
+ public void deleteAttachmentFilesForMessage(long mmsId) {
+ SQLiteDatabase database = databaseHelper.getWritableDatabase();
+ Cursor cursor = null;
+
+ try {
+ cursor = database.query(TABLE_NAME, new String[] {DATA, THUMBNAIL, CONTENT_TYPE}, MMS_ID + " = ?",
+ new String[] {mmsId+""}, null, null, null);
+
+ while (cursor != null && cursor.moveToNext()) {
+ deleteAttachmentOnDisk(cursor.getString(0), cursor.getString(1), cursor.getString(2));
+ }
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(DATA, (String) null);
+ values.put(DATA_RANDOM, (byte[]) null);
+ values.put(THUMBNAIL, (String) null);
+ values.put(THUMBNAIL_RANDOM, (byte[]) null);
+ values.put(FILE_NAME, (String) null);
+ values.put(CAPTION, (String) null);
+ values.put(SIZE, 0);
+ values.put(WIDTH, 0);
+ values.put(HEIGHT, 0);
+ values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
+
+ database.update(TABLE_NAME, values, MMS_ID + " = ?", new String[] {mmsId + ""});
+ notifyAttachmentListeners();
+
+ long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(mmsId);
+ if (threadId > 0) {
+ notifyConversationListeners(threadId);
+ }
+ }
+
+
public void deleteAttachment(@NonNull AttachmentId id) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
diff --git a/src/org/thoughtcrime/securesms/database/MediaDatabase.java b/src/org/thoughtcrime/securesms/database/MediaDatabase.java
index 523a168c57..d169f26568 100644
--- a/src/org/thoughtcrime/securesms/database/MediaDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MediaDatabase.java
@@ -47,6 +47,7 @@ public class MediaDatabase extends Database {
+ "WHERE " + AttachmentDatabase.MMS_ID + " IN (SELECT " + MmsSmsColumns.ID
+ " FROM " + MmsDatabase.TABLE_NAME
+ " WHERE " + MmsDatabase.THREAD_ID + " = ?) AND (%s) AND "
+ + MmsDatabase.REVEAL_DURATION + " = 0 AND "
+ AttachmentDatabase.DATA + " IS NOT NULL AND "
+ AttachmentDatabase.QUOTE + " = 0 AND "
+ AttachmentDatabase.STICKER_PACK_ID + " IS NULL "
diff --git a/src/org/thoughtcrime/securesms/database/MessagingDatabase.java b/src/org/thoughtcrime/securesms/database/MessagingDatabase.java
index 349ad48255..23bb816ac2 100644
--- a/src/org/thoughtcrime/securesms/database/MessagingDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MessagingDatabase.java
@@ -5,15 +5,19 @@ import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
+import androidx.annotation.NonNull;
+
import net.sqlcipher.database.SQLiteDatabase;
import org.thoughtcrime.securesms.database.documents.Document;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
+import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.whispersystems.libsignal.IdentityKey;
+import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.ArrayList;
diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
index c2870723b3..4886c768c7 100644
--- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -63,6 +63,8 @@ import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
+import org.thoughtcrime.securesms.revealable.RevealExpirationInfo;
+import org.thoughtcrime.securesms.revealable.RevealableUtil;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@@ -107,6 +109,9 @@ public class MmsDatabase extends MessagingDatabase {
static final String SHARED_CONTACTS = "shared_contacts";
static final String LINK_PREVIEWS = "previews";
+ public static final String REVEAL_DURATION = "reveal_duration";
+ public static final String REVEAL_START_TIME = "reveal_start_time";
+
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
THREAD_ID + " INTEGER, " + DATE_SENT + " INTEGER, " + DATE_RECEIVED + " INTEGER, " + MESSAGE_BOX + " INTEGER, " +
READ + " INTEGER DEFAULT 0, " + "m_id" + " TEXT, " + "sub" + " TEXT, " +
@@ -126,7 +131,7 @@ public class MmsDatabase extends MessagingDatabase {
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + QUOTE_ID + " INTEGER DEFAULT 0, " +
QUOTE_AUTHOR + " TEXT, " + QUOTE_BODY + " TEXT, " + QUOTE_ATTACHMENT + " INTEGER DEFAULT -1, " +
QUOTE_MISSING + " INTEGER DEFAULT 0, " + SHARED_CONTACTS + " TEXT, " + UNIDENTIFIED + " INTEGER DEFAULT 0, " +
- LINK_PREVIEWS + " TEXT);";
+ LINK_PREVIEWS + " TEXT, " + REVEAL_DURATION + " INTEGER DEFAULT 0, " + REVEAL_START_TIME + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
@@ -147,7 +152,7 @@ public class MmsDatabase extends MessagingDatabase {
BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID,
DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID,
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING,
- SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED,
+ SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, REVEAL_DURATION, REVEAL_START_TIME,
"json_group_array(json_object(" +
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
@@ -432,6 +437,60 @@ public class MmsDatabase extends MessagingDatabase {
notifyConversationListeners(threadId);
}
+ public void markRevealStarted(long messageId) {
+ markRevealStarted(messageId, System.currentTimeMillis());
+ }
+
+ public void markRevealStarted(long messageId, long startTime) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(REVEAL_START_TIME, startTime);
+
+ SQLiteDatabase db = databaseHelper.getWritableDatabase();
+ db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
+
+ long threadId = getThreadIdForMessage(messageId);
+ notifyConversationListeners(threadId);
+ }
+
+ public List markRevealStarted(@NonNull SyncMessageId messageId, long proposedStartTime) {
+ SQLiteDatabase db = databaseHelper.getWritableDatabase();
+ List expirationInfos = new LinkedList<>();
+
+ String[] projection = new String[] { ID, ADDRESS, THREAD_ID, DATE_SENT, DATE_RECEIVED, REVEAL_DURATION, REVEAL_START_TIME };
+ String selection = DATE_SENT + " = ?";
+ String[] args = new String[] { String.valueOf(messageId.getTimetamp()) };
+
+ try (Cursor cursor = db.query(TABLE_NAME, projection, selection, args, null, null, null)) {
+ while (cursor != null && cursor.moveToNext()) {
+ Address theirAddress = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)));
+ Address ourAddress = messageId.getAddress();
+
+ if (ourAddress.equals(theirAddress) || theirAddress.isGroup()) {
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
+ long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID));
+ long receiveTime = cursor.getLong(cursor.getColumnIndexOrThrow(DATE_RECEIVED));
+ long revealDuration = cursor.getLong(cursor.getColumnIndexOrThrow(REVEAL_DURATION));
+ long revealStartTime = cursor.getLong(cursor.getColumnIndexOrThrow(REVEAL_START_TIME));
+
+ revealStartTime = revealStartTime > 0 ? Math.min(proposedStartTime, revealStartTime) : proposedStartTime;
+ revealStartTime = Math.min(revealStartTime, System.currentTimeMillis());
+
+ ContentValues values = new ContentValues();
+
+ values.put(REVEAL_START_TIME, revealStartTime);
+ expirationInfos.add(new RevealExpirationInfo(id, receiveTime, revealStartTime, revealDuration));
+
+ db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(id) });
+
+ DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId);
+ notifyConversationListeners(threadId);
+ }
+ }
+ }
+
+ return expirationInfos;
+ }
+
public void markAsNotified(long id) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
@@ -609,6 +668,7 @@ public class MmsDatabase extends MessagingDatabase {
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT));
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID));
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN));
+ long revealDuration = cursor.getLong(cursor.getColumnIndexOrThrow(REVEAL_DURATION));
String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID));
int distributionType = DatabaseFactory.getThreadDatabase(context).getDistributionType(threadId);
@@ -655,12 +715,12 @@ public class MmsDatabase extends MessagingDatabase {
}
if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) {
- return new OutgoingGroupMediaMessage(recipient, body, attachments, timestamp, 0, quote, contacts, previews);
+ return new OutgoingGroupMediaMessage(recipient, body, attachments, timestamp, 0, 0, quote, contacts, previews);
} else if (Types.isExpirationTimerUpdate(outboxType)) {
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
}
- OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, distributionType, quote, contacts, previews, networkFailures, mismatches);
+ OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, revealDuration, distributionType, quote, contacts, previews, networkFailures, mismatches);
if (Types.isSecureType(outboxType)) {
return new OutgoingSecureMediaMessage(message);
@@ -764,6 +824,7 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(READ, 1);
contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT));
contentValues.put(EXPIRES_IN, request.getExpiresIn());
+ contentValues.put(REVEAL_DURATION, request.getRevealDuration());
List attachments = new LinkedList<>();
@@ -831,6 +892,7 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(PART_COUNT, retrieved.getAttachments().size());
contentValues.put(SUBSCRIPTION_ID, retrieved.getSubscriptionId());
contentValues.put(EXPIRES_IN, retrieved.getExpiresIn());
+ contentValues.put(REVEAL_DURATION, retrieved.getRevealDuration());
contentValues.put(READ, retrieved.isExpirationUpdate() ? 1 : 0);
contentValues.put(UNIDENTIFIED, retrieved.isUnidentified());
@@ -986,6 +1048,7 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(DATE_RECEIVED, System.currentTimeMillis());
contentValues.put(SUBSCRIPTION_ID, message.getSubscriptionId());
contentValues.put(EXPIRES_IN, message.getExpiresIn());
+ contentValues.put(REVEAL_DURATION, message.getRevealDuration());
contentValues.put(ADDRESS, message.getRecipient().getAddress().serialize());
contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(Long::longValue).sum());
contentValues.put(READ_RECEIPT_COUNT, Stream.of(earlyReadReceipts.values()).mapToLong(Long::longValue).sum());
@@ -1244,6 +1307,42 @@ public class MmsDatabase extends MessagingDatabase {
database.delete(TABLE_NAME, null, null);
}
+ public @Nullable RevealExpirationInfo getNearestExpiringRevealableMessage() {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ RevealExpirationInfo info = null;
+ long nearestExpiration = Long.MAX_VALUE;
+
+ String query = "SELECT " +
+ TABLE_NAME + "." + ID + ", " +
+ REVEAL_DURATION + ", " +
+ REVEAL_START_TIME + ", " +
+ DATE_RECEIVED + " " +
+ "FROM " + TABLE_NAME + " INNER JOIN " + AttachmentDatabase.TABLE_NAME + " " +
+ "ON " + TABLE_NAME + "." + ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " " +
+ "WHERE " +
+ REVEAL_DURATION + " > 0 AND " +
+ "(" + AttachmentDatabase.DATA + " NOT NULL OR " + AttachmentDatabase.TRANSFER_STATE + " != ?)";
+ String[] args = new String[] { String.valueOf(AttachmentDatabase.TRANSFER_PROGRESS_DONE) };
+
+ try (Cursor cursor = db.rawQuery(query, args)) {
+ while (cursor != null && cursor.moveToNext()) {
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
+ long revealDuration = cursor.getLong(cursor.getColumnIndexOrThrow(REVEAL_DURATION));
+ long revealStartTime = cursor.getLong(cursor.getColumnIndexOrThrow(REVEAL_START_TIME));
+ long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(DATE_RECEIVED));
+ long expiresAt = revealStartTime > 0 ? revealStartTime + revealDuration
+ : dateReceived + RevealableUtil.MAX_LIFESPAN;
+
+ if (info == null || expiresAt < nearestExpiration) {
+ info = new RevealExpirationInfo(id, dateReceived, revealStartTime, revealDuration);
+ nearestExpiration = expiresAt;
+ }
+ }
+ }
+
+ return info;
+ }
+
public Cursor getCarrierMmsInformation(String apn) {
Uri uri = Uri.withAppendedPath(Uri.parse("content://telephony/carriers"), "current");
String selection = TextUtils.isEmpty(apn) ? null : "apn = ?";
@@ -1342,7 +1441,10 @@ public class MmsDatabase extends MessagingDatabase {
new LinkedList(),
message.getSubscriptionId(),
message.getExpiresIn(),
- System.currentTimeMillis(), 0,
+ System.currentTimeMillis(),
+ message.getRevealDuration(),
+ 0,
+ 0,
message.getOutgoingQuote() != null ?
new Quote(message.getOutgoingQuote().getId(),
message.getOutgoingQuote().getAuthor(),
@@ -1439,6 +1541,8 @@ public class MmsDatabase extends MessagingDatabase {
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRES_IN));
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRE_STARTED));
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.UNIDENTIFIED)) == 1;
+ long revealDuration = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.REVEAL_DURATION));
+ long revealStartTime = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.REVEAL_START_TIME));
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
readReceiptCount = 0;
@@ -1459,6 +1563,7 @@ public class MmsDatabase extends MessagingDatabase {
addressDeviceId, dateSent, dateReceived, deliveryReceiptCount,
threadId, body, slideDeck, partCount, box, mismatches,
networkFailures, subscriptionId, expiresIn, expireStarted,
+ revealDuration, revealStartTime,
readReceiptCount, quote, contacts, previews, unidentified);
}
diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java
index 118989623e..7053d3bd93 100644
--- a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java
+++ b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java
@@ -32,6 +32,7 @@ public interface MmsSmsColumns {
protected static final long MISSED_CALL_TYPE = 3;
protected static final long JOINED_TYPE = 4;
protected static final long UNSUPPORTED_MESSAGE_TYPE = 5;
+ protected static final long INVALID_MESSAGE_TYPE = 6;
protected static final long BASE_INBOX_TYPE = 20;
protected static final long BASE_OUTBOX_TYPE = 21;
@@ -147,6 +148,10 @@ public interface MmsSmsColumns {
return (type & BASE_TYPE_MASK) == UNSUPPORTED_MESSAGE_TYPE;
}
+ public static boolean isInvalidMessageType(long type) {
+ return (type & BASE_TYPE_MASK) == INVALID_MESSAGE_TYPE;
+ }
+
public static boolean isSecureType(long type) {
return (type & SECURE_MESSAGE_BIT) != 0;
}
diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
index 1d6fe88903..dfa9614d98 100644
--- a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
@@ -70,7 +70,9 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.QUOTE_MISSING,
MmsDatabase.QUOTE_ATTACHMENT,
MmsDatabase.SHARED_CONTACTS,
- MmsDatabase.LINK_PREVIEWS};
+ MmsDatabase.LINK_PREVIEWS,
+ MmsDatabase.REVEAL_DURATION,
+ MmsDatabase.REVEAL_START_TIME};
public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
@@ -270,7 +272,9 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.QUOTE_MISSING,
MmsDatabase.QUOTE_ATTACHMENT,
MmsDatabase.SHARED_CONTACTS,
- MmsDatabase.LINK_PREVIEWS};
+ MmsDatabase.LINK_PREVIEWS,
+ MmsDatabase.REVEAL_DURATION,
+ MmsDatabase.REVEAL_START_TIME};
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
@@ -296,7 +300,9 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.QUOTE_MISSING,
MmsDatabase.QUOTE_ATTACHMENT,
MmsDatabase.SHARED_CONTACTS,
- MmsDatabase.LINK_PREVIEWS};
+ MmsDatabase.LINK_PREVIEWS,
+ MmsDatabase.REVEAL_DURATION,
+ MmsDatabase.REVEAL_START_TIME};
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
@@ -367,6 +373,8 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsDatabase.QUOTE_ATTACHMENT);
mmsColumnsPresent.add(MmsDatabase.SHARED_CONTACTS);
mmsColumnsPresent.add(MmsDatabase.LINK_PREVIEWS);
+ mmsColumnsPresent.add(MmsDatabase.REVEAL_DURATION);
+ mmsColumnsPresent.add(MmsDatabase.REVEAL_START_TIME);
Set smsColumnsPresent = new HashSet<>();
smsColumnsPresent.add(MmsSmsColumns.ID);
diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
index f42237571c..81527845b5 100644
--- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -221,6 +221,10 @@ public class SmsDatabase extends MessagingDatabase {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.UNSUPPORTED_MESSAGE_TYPE);
}
+ public void markAsInvalidMessage(long id) {
+ updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.INVALID_MESSAGE_TYPE);
+ }
+
public void markAsLegacyVersion(long id) {
updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_LEGACY_BIT);
}
@@ -883,7 +887,8 @@ public class SmsDatabase extends MessagingDatabase {
addressDeviceId,
dateSent, dateReceived, deliveryReceiptCount, type,
threadId, status, mismatches, subscriptionId,
- expiresIn, expireStarted, readReceiptCount, unidentified);
+ expiresIn, expireStarted,
+ readReceiptCount, unidentified);
}
private List getMismatches(String document) {
diff --git a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java
index 0bd67c024e..89ee2cf2d6 100644
--- a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java
@@ -26,6 +26,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
+import com.fasterxml.jackson.annotation.JsonProperty;
import net.sqlcipher.database.SQLiteDatabase;
@@ -44,12 +45,15 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.DelimiterUtil;
+import org.thoughtcrime.securesms.util.JsonUtils;
+import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.Closeable;
+import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -71,6 +75,8 @@ public class ThreadDatabase extends Database {
private static final String ERROR = "error";
public static final String SNIPPET_TYPE = "snippet_type";
public static final String SNIPPET_URI = "snippet_uri";
+ public static final String SNIPPET_CONTENT_TYPE = "snippet_content_type";
+ public static final String SNIPPET_EXTRAS = "snippet_extras";
public static final String ARCHIVED = "archived";
public static final String STATUS = "status";
public static final String DELIVERY_RECEIPT_COUNT = "delivery_receipt_count";
@@ -85,6 +91,7 @@ public class ThreadDatabase extends Database {
SNIPPET_CHARSET + " INTEGER DEFAULT 0, " + READ + " INTEGER DEFAULT 1, " +
TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " +
+ SNIPPET_CONTENT_TYPE + " TEXT DEFAULT NULL, " + SNIPPET_EXTRAS + " TEXT DEFAULT NULL, " +
ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " +
DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
LAST_SEEN + " INTEGER DEFAULT 0, " + HAS_SENT + " INTEGER DEFAULT 0, " +
@@ -97,7 +104,7 @@ public class ThreadDatabase extends Database {
private static final String[] THREAD_PROJECTION = {
ID, DATE, MESSAGE_COUNT, ADDRESS, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, TYPE, ERROR, SNIPPET_TYPE,
- SNIPPET_URI, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT
+ SNIPPET_URI, SNIPPET_CONTENT_TYPE, SNIPPET_EXTRAS, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT
};
private static final List TYPED_THREAD_PROJECTION = Stream.of(THREAD_PROJECTION)
@@ -130,15 +137,28 @@ public class ThreadDatabase extends Database {
}
private void updateThread(long threadId, long count, String body, @Nullable Uri attachment,
+ @Nullable String contentType, @Nullable Extra extra,
long date, int status, int deliveryReceiptCount, long type, boolean unarchive,
long expiresIn, int readReceiptCount)
{
+ String extraSerialized = null;
+
+ if (extra != null) {
+ try {
+ extraSerialized = JsonUtils.toJson(extra);
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
ContentValues contentValues = new ContentValues(7);
contentValues.put(DATE, date - date % 1000);
contentValues.put(MESSAGE_COUNT, count);
contentValues.put(SNIPPET, body);
contentValues.put(SNIPPET_URI, attachment == null ? null : attachment.toString());
contentValues.put(SNIPPET_TYPE, type);
+ contentValues.put(SNIPPET_CONTENT_TYPE, contentType);
+ contentValues.put(SNIPPET_EXTRAS, extraSerialized);
contentValues.put(STATUS, status);
contentValues.put(DELIVERY_RECEIPT_COUNT, deliveryReceiptCount);
contentValues.put(READ_RECEIPT_COUNT, readReceiptCount);
@@ -571,6 +591,7 @@ public class ThreadDatabase extends Database {
if (reader != null && (record = reader.getNext()) != null) {
updateThread(threadId, count, getFormattedBodyFor(record), getAttachmentUriFor(record),
+ getContentTypeFor(record), getExtrasFor(record),
record.getTimestamp(), record.getDeliveryStatus(), record.getDeliveryReceiptCount(),
record.getType(), unarchive, record.getExpiresIn(), record.getReadReceiptCount());
notifyConversationListListeners();
@@ -601,13 +622,36 @@ public class ThreadDatabase extends Database {
SlideDeck slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
Slide thumbnail = slideDeck.getThumbnailSlide();
- if (thumbnail != null) {
+ if (thumbnail != null && ((MmsMessageRecord) record).getRevealDuration() == 0) {
return thumbnail.getThumbnailUri();
}
return null;
}
+ private @Nullable String getContentTypeFor(MessageRecord record) {
+ if (record.isMms()) {
+ SlideDeck slideDeck = ((MmsMessageRecord) record).getSlideDeck();
+
+ if (slideDeck.getSlides().size() > 0) {
+ return slideDeck.getSlides().get(0).getContentType();
+ }
+ }
+
+ return null;
+ }
+
+ private @Nullable Extra getExtrasFor(MessageRecord record) {
+ if (record.isMms() && ((MmsMessageRecord) record).getRevealDuration() > 0) {
+ return Extra.forRevealableMessage();
+ } else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) {
+ return Extra.forSticker();
+ } else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getSlides().size() > 1) {
+ return Extra.forAlbum();
+ }
+ return null;
+ }
+
private @NonNull String createQuery(@NonNull String where, int limit) {
String projection = Util.join(COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION, ",");
String query =
@@ -686,12 +730,24 @@ public class ThreadDatabase extends Database {
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN));
long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN));
Uri snippetUri = getSnippetUri(cursor);
+ String contentType = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_CONTENT_TYPE));
+ String extraString = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_EXTRAS));
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
readReceiptCount = 0;
}
- return new ThreadRecord(body, snippetUri, recipient, date, count,
+ Extra extra = null;
+
+ if (extraString != null) {
+ try {
+ extra = JsonUtils.fromJson(extraString, Extra.class);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to decode extras!");
+ }
+ }
+
+ return new ThreadRecord(body, snippetUri, contentType, extra, recipient, date, count,
unreadCount, threadId, deliveryReceiptCount, status, type,
distributionType, archived, expiresIn, lastSeen, readReceiptCount);
}
@@ -716,4 +772,45 @@ public class ThreadDatabase extends Database {
}
}
}
+
+ public static final class Extra {
+
+ @JsonProperty private final boolean isRevealable;
+ @JsonProperty private final boolean isSticker;
+ @JsonProperty private final boolean isAlbum;
+
+ public Extra(@JsonProperty("isRevealable") boolean isRevealable,
+ @JsonProperty("isSticker") boolean isSticker,
+ @JsonProperty("isAlbum") boolean isAlbum)
+ {
+ this.isRevealable = isRevealable;
+ this.isSticker = isSticker;
+ this.isAlbum = isAlbum;
+ }
+
+ public static @NonNull Extra forRevealableMessage() {
+ return new Extra(true, false, false);
+ }
+
+ public static @NonNull Extra forSticker() {
+ return new Extra(false, true, false);
+ }
+
+ public static @NonNull Extra forAlbum() {
+ return new Extra(false, false, true);
+ }
+
+
+ public boolean isRevealable() {
+ return isRevealable;
+ }
+
+ public boolean isSticker() {
+ return isSticker;
+ }
+
+ public boolean isAlbum() {
+ return isAlbum;
+ }
+ }
}
diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
index 678ec09a7d..67aafa94a8 100644
--- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
+++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
@@ -66,8 +66,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int RECIPIENT_FORCE_SMS_SELECTION = 19;
private static final int JOBMANAGER_STRIKES_BACK = 20;
private static final int STICKERS = 21;
+ private static final int REVEALABLE_MESSAGES = 22;
- private static final int DATABASE_VERSION = 21;
+ private static final int DATABASE_VERSION = 22;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -462,6 +463,14 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("CREATE INDEX IF NOT EXISTS part_sticker_pack_id_index ON part (sticker_pack_id)");
}
+ if (oldVersion < REVEALABLE_MESSAGES) {
+ db.execSQL("ALTER TABLE mms ADD COLUMN reveal_duration INTEGER DEFAULT 0");
+ db.execSQL("ALTER TABLE mms ADD COLUMN reveal_start_time INTEGER DEFAULT 0");
+
+ db.execSQL("ALTER TABLE thread ADD COLUMN snippet_content_type TEXT DEFAULT NULL");
+ db.execSQL("ALTER TABLE thread ADD COLUMN snippet_extras TEXT DEFAULT NULL");
+ }
+
db.setTransactionSuccessful();
} finally {
db.endTransaction();
diff --git a/src/org/thoughtcrime/securesms/database/loaders/ConversationListLoader.java b/src/org/thoughtcrime/securesms/database/loaders/ConversationListLoader.java
index f2dd7081cf..c57e2b03c9 100644
--- a/src/org/thoughtcrime/securesms/database/loaders/ConversationListLoader.java
+++ b/src/org/thoughtcrime/securesms/database/loaders/ConversationListLoader.java
@@ -44,6 +44,7 @@ public class ConversationListLoader extends AbstractCursorLoader {
ThreadDatabase.ID, ThreadDatabase.DATE, ThreadDatabase.MESSAGE_COUNT,
ThreadDatabase.ADDRESS, ThreadDatabase.SNIPPET, ThreadDatabase.READ, ThreadDatabase.UNREAD_COUNT,
ThreadDatabase.TYPE, ThreadDatabase.SNIPPET_TYPE, ThreadDatabase.SNIPPET_URI,
+ ThreadDatabase.SNIPPET_CONTENT_TYPE, ThreadDatabase.SNIPPET_EXTRAS,
ThreadDatabase.ARCHIVED, ThreadDatabase.STATUS, ThreadDatabase.DELIVERY_RECEIPT_COUNT,
ThreadDatabase.EXPIRES_IN, ThreadDatabase.LAST_SEEN, ThreadDatabase.READ_RECEIPT_COUNT}, 1);
@@ -56,7 +57,7 @@ public class ConversationListLoader extends AbstractCursorLoader {
switchToArchiveCursor.addRow(new Object[] {-1L, System.currentTimeMillis(), archivedCount,
"-1", null, 1, 0, ThreadDatabase.DistributionTypes.ARCHIVE,
- 0, null, 0, -1, 0, 0, 0, -1});
+ 0, null, null, null, 0, -1, 0, 0, 0, -1});
cursorList.add(switchToArchiveCursor);
}
diff --git a/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java
index cfe7d2acd8..3ad7384af9 100644
--- a/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java
@@ -54,14 +54,15 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
int partCount, long mailbox,
List mismatches,
List failures, int subscriptionId,
- long expiresIn, long expireStarted, int readReceiptCount,
+ long expiresIn, long expireStarted,
+ long revealDuration, long revealStartTime, int readReceiptCount,
@Nullable Quote quote, @NonNull List contacts,
@NonNull List linkPreviews, boolean unidentified)
{
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
- subscriptionId, expiresIn, expireStarted, slideDeck, readReceiptCount, quote, contacts,
- linkPreviews, unidentified);
+ subscriptionId, expiresIn, expireStarted, revealDuration, revealStartTime, slideDeck,
+ readReceiptCount, quote, contacts, linkPreviews, unidentified);
this.partCount = partCount;
}
diff --git a/src/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java
index 679c8f15f5..a4bf04975b 100644
--- a/src/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java
@@ -22,19 +22,25 @@ public abstract class MmsMessageRecord extends MessageRecord {
private final @NonNull List contacts = new LinkedList<>();
private final @NonNull List linkPreviews = new LinkedList<>();
+ private final long revealDuration;
+ private final long revealStartTime;
+
MmsMessageRecord(long id, String body, Recipient conversationRecipient,
Recipient individualRecipient, int recipientDeviceId, long dateSent,
long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount,
long type, List mismatches,
List networkFailures, int subscriptionId, long expiresIn,
- long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount,
+ long expireStarted, long revealDuration, long revealStartTime,
+ @NonNull SlideDeck slideDeck, int readReceiptCount,
@Nullable Quote quote, @NonNull List contacts,
@NonNull List linkPreviews, boolean unidentified)
{
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount, unidentified);
- this.slideDeck = slideDeck;
- this.quote = quote;
+ this.slideDeck = slideDeck;
+ this.quote = quote;
+ this.revealDuration = revealDuration;
+ this.revealStartTime = revealStartTime;
this.contacts.addAll(contacts);
this.linkPreviews.addAll(linkPreviews);
@@ -76,4 +82,12 @@ public abstract class MmsMessageRecord extends MessageRecord {
public @NonNull List getLinkPreviews() {
return linkPreviews;
}
+
+ public long getRevealDuration() {
+ return revealDuration;
+ }
+
+ public long getRevealStartTime() {
+ return revealStartTime;
+ }
}
diff --git a/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java
index 83212609bc..fbdafae288 100644
--- a/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java
@@ -57,7 +57,7 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
super(id, "", conversationRecipient, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
new LinkedList(), new LinkedList(), subscriptionId,
- 0, 0, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false);
+ 0, 0, 0, 0, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false);
this.contentLocation = contentLocation;
this.messageSize = messageSize;
diff --git a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java
index 0b5e6801a9..09b16f1ae2 100644
--- a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java
@@ -86,6 +86,8 @@ public class SmsMessageRecord extends MessageRecord {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset_s, getIndividualRecipient().toShortString()));
} else if (SmsDatabase.Types.isUnsupportedMessageType(type)) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_this_message_could_not_be_processed_because_it_was_sent_from_a_newer_version));
+ } else if (SmsDatabase.Types.isInvalidMessageType(type)) {
+ return emphasisAdded(context.getString(R.string.SmsMessageRecord_error_handling_incoming_message));
} else {
return super.getDisplayBody(context);
}
diff --git a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java
index 2ff72763ae..f735b20e19 100644
--- a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java
@@ -29,8 +29,11 @@ import android.text.style.StyleSpan;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
+import org.thoughtcrime.securesms.database.ThreadDatabase;
+import org.thoughtcrime.securesms.database.ThreadDatabase.Extra;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ExpirationUtil;
+import org.thoughtcrime.securesms.util.MediaUtil;
/**
* The message record model which represents thread heading messages.
@@ -41,6 +44,8 @@ import org.thoughtcrime.securesms.util.ExpirationUtil;
public class ThreadRecord extends DisplayRecord {
private @Nullable final Uri snippetUri;
+ private @Nullable final String contentType;
+ private @Nullable final Extra extra;
private final long count;
private final int unreadCount;
private final int distributionType;
@@ -49,6 +54,7 @@ public class ThreadRecord extends DisplayRecord {
private final long lastSeen;
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
+ @Nullable String contentType, @Nullable Extra extra,
@NonNull Recipient recipient, long date, long count, int unreadCount,
long threadId, int deliveryReceiptCount, int status, long snippetType,
int distributionType, boolean archived, long expiresIn, long lastSeen,
@@ -56,6 +62,8 @@ public class ThreadRecord extends DisplayRecord {
{
super(body, recipient, date, date, threadId, status, deliveryReceiptCount, snippetType, readReceiptCount);
this.snippetUri = snippetUri;
+ this.contentType = contentType;
+ this.extra = extra;
this.count = count;
this.unreadCount = unreadCount;
this.distributionType = distributionType;
@@ -113,7 +121,13 @@ public class ThreadRecord extends DisplayRecord {
return emphasisAdded(context.getString(R.string.ThreadRecord_message_could_not_be_processed));
} else {
if (TextUtils.isEmpty(getBody())) {
- return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
+ if (extra != null && extra.isSticker()) {
+ return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_sticker)));
+ } else if (extra != null && extra.isRevealable() && MediaUtil.isImageType(contentType)) {
+ return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_photo)));
+ } else {
+ return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
+ }
} else {
return new SpannableString(getBody());
}
diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java
index ceb97c96e3..7ec883a459 100644
--- a/src/org/thoughtcrime/securesms/groups/GroupManager.java
+++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java
@@ -115,7 +115,7 @@ public class GroupManager {
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null);
}
- OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, null, Collections.emptyList(), Collections.emptyList());
+ OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, 0, null, Collections.emptyList(), Collections.emptyList());
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
return new GroupActionResult(groupRecipient, threadId);
diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java
index 054e546545..c7eb6cc595 100644
--- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java
+++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java
@@ -212,7 +212,7 @@ public class GroupMessageProcessor {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
Address addres = Address.fromExternal(context, GroupUtil.getEncodedId(group.getGroupId(), false));
Recipient recipient = Recipient.from(context, addres, false);
- OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList(), Collections.emptyList());
+ OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, 0, null, Collections.emptyList(), Collections.emptyList());
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
@@ -222,7 +222,7 @@ public class GroupMessageProcessor {
} else {
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
String body = Base64.encodeBytes(storage.toByteArray());
- IncomingTextMessage incoming = new IncomingTextMessage(Address.fromExternal(context, content.getSender()), content.getSenderDevice(), content.getTimestamp(), body, Optional.of(group), 0, content.isNeedsReceipt());
+ IncomingTextMessage incoming = new IncomingTextMessage(Address.fromExternal(context, content.getSender()), content.getSenderDevice(), content.getTimestamp(), body, Optional.of(group), 0, 0, content.isNeedsReceipt());
IncomingGroupMessage groupMessage = new IncomingGroupMessage(incoming, storage, body);
Optional insertResult = smsDatabase.insertMessageInbox(groupMessage);
diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
index cd23905a30..664676f3e2 100644
--- a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
+++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
@@ -40,6 +40,7 @@ public final class JobManagerFactories {
put(MultiDeviceGroupUpdateJob.KEY, new MultiDeviceGroupUpdateJob.Factory());
put(MultiDeviceProfileKeyUpdateJob.KEY, new MultiDeviceProfileKeyUpdateJob.Factory());
put(MultiDeviceReadUpdateJob.KEY, new MultiDeviceReadUpdateJob.Factory());
+ put(MultiDeviceRevealUpdateJob.KEY, new MultiDeviceRevealUpdateJob.Factory());
put(MultiDeviceStickerPackOperationJob.KEY, new MultiDeviceStickerPackOperationJob.Factory());
put(MultiDeviceStickerPackSyncJob.KEY, new MultiDeviceStickerPackSyncJob.Factory());
put(MultiDeviceVerifiedUpdateJob.KEY, new MultiDeviceVerifiedUpdateJob.Factory());
diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
index 5c340b4184..ca57a3cc5a 100644
--- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
@@ -247,7 +247,7 @@ public class MmsDownloadJob extends BaseJob {
group = Optional.of(Address.fromSerialized(DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(new LinkedList<>(members), true)));
}
- IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, attachments, subscriptionId, 0, false, false);
+ IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, attachments, subscriptionId, 0, false, 0, false);
Optional insertResult = database.insertMessageInbox(message, contentLocation, threadId);
if (insertResult.isPresent()) {
diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceRevealUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceRevealUpdateJob.java
new file mode 100644
index 0000000000..a14015e10e
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceRevealUpdateJob.java
@@ -0,0 +1,124 @@
+package org.thoughtcrime.securesms.jobs;
+
+import androidx.annotation.NonNull;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
+import org.thoughtcrime.securesms.database.Address;
+import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.jobmanager.Data;
+import org.thoughtcrime.securesms.jobmanager.Job;
+import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.util.JsonUtils;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.whispersystems.signalservice.api.SignalServiceMessageSender;
+import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
+import org.whispersystems.signalservice.api.messages.multidevice.MessageTimerReadMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
+import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.concurrent.TimeUnit;
+
+public class MultiDeviceRevealUpdateJob extends BaseJob {
+
+ public static final String KEY = "MultiDeviceRevealUpdateJob";
+
+ private static final String TAG = MultiDeviceRevealUpdateJob.class.getSimpleName();
+
+ private static final String KEY_MESSAGE_ID = "message_id";
+
+ private SerializableSyncMessageId messageId;
+
+ public MultiDeviceRevealUpdateJob(SyncMessageId messageId) {
+ this(new Parameters.Builder()
+ .addConstraint(NetworkConstraint.KEY)
+ .setLifespan(TimeUnit.DAYS.toMillis(1))
+ .setMaxAttempts(Parameters.UNLIMITED)
+ .build(),
+ messageId);
+ }
+
+ private MultiDeviceRevealUpdateJob(@NonNull Parameters parameters, @NonNull SyncMessageId syncMessageId) {
+ super(parameters);
+ this.messageId = new SerializableSyncMessageId(syncMessageId.getAddress().toPhoneString(), syncMessageId.getTimetamp());
+ }
+
+ @Override
+ public @NonNull Data serialize() {
+ String serialized;
+
+ try {
+ serialized = JsonUtils.toJson(messageId);
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+
+ return new Data.Builder().putString(KEY_MESSAGE_ID, serialized).build();
+ }
+
+ @Override
+ public @NonNull String getFactoryKey() {
+ return KEY;
+ }
+
+ @Override
+ public void onRun() throws IOException, UntrustedIdentityException {
+ if (!TextSecurePreferences.isMultiDevice(context)) {
+ Log.i(TAG, "Not multi device...");
+ return;
+ }
+
+ SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
+ MessageTimerReadMessage timerMessage = new MessageTimerReadMessage(messageId.sender, messageId.timestamp);
+
+ messageSender.sendMessage(SignalServiceSyncMessage.forMessageTimerRead(timerMessage), UnidentifiedAccessUtil.getAccessForSync(context));
+ }
+
+ @Override
+ public boolean onShouldRetry(@NonNull Exception exception) {
+ return exception instanceof PushNetworkException;
+ }
+
+ @Override
+ public void onCanceled() {
+
+ }
+
+ private static class SerializableSyncMessageId implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ @JsonProperty
+ private final String sender;
+
+ @JsonProperty
+ private final long timestamp;
+
+ private SerializableSyncMessageId(@JsonProperty("sender") String sender, @JsonProperty("timestamp") long timestamp) {
+ this.sender = sender;
+ this.timestamp = timestamp;
+ }
+ }
+
+ public static final class Factory implements Job.Factory {
+ @Override
+ public @NonNull MultiDeviceRevealUpdateJob create(@NonNull Parameters parameters, @NonNull Data data) {
+ SerializableSyncMessageId messageId;
+
+ try {
+ messageId = JsonUtils.fromJson(data.getString(KEY_MESSAGE_ID), SerializableSyncMessageId.class);
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+
+ SyncMessageId syncMessageId = new SyncMessageId(Address.fromSerialized(messageId.sender), messageId.timestamp);
+
+ return new MultiDeviceRevealUpdateJob(parameters, syncMessageId);
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
index 6cd93b39fd..07c5dc3ad4 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
@@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.attachments.PointerAttachment;
+import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
@@ -79,6 +80,8 @@ import org.thoughtcrime.securesms.mms.StickerSlide;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.revealable.RevealExpirationInfo;
+import org.thoughtcrime.securesms.revealable.RevealableMessageManager;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
@@ -109,6 +112,7 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.MessageTimerReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
@@ -253,7 +257,8 @@ public class PushDecryptJob extends BaseJob {
SignalServiceDataMessage message = content.getDataMessage().get();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
- if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
+ if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), message.getGroupInfo(), content.getTimestamp(), smsMessageId);
+ else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId);
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
@@ -278,6 +283,7 @@ public class PushDecryptJob extends BaseJob {
if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(content, syncMessage.getSent().get());
else if (syncMessage.getRequest().isPresent()) handleSynchronizeRequestMessage(syncMessage.getRequest().get());
else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(syncMessage.getRead().get(), content.getTimestamp());
+ else if (syncMessage.getMessageTimerRead().isPresent()) handleSynchronizeMessageTimerReadMessage(syncMessage.getMessageTimerRead().get(), content.getTimestamp());
else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(syncMessage.getVerified().get());
else if (syncMessage.getStickerPackOperations().isPresent()) handleSynchronizeStickerPackOperation(syncMessage.getStickerPackOperations().get());
else Log.w(TAG, "Contains no known sync types...");
@@ -425,7 +431,7 @@ public class PushDecryptJob extends BaseJob {
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(Address.fromExternal(context, content.getSender()),
content.getSenderDevice(),
content.getTimestamp(),
- "", Optional.absent(), 0,
+ "", Optional.absent(), 0, 0,
content.isNeedsReceipt());
Long threadId;
@@ -509,6 +515,7 @@ public class PushDecryptJob extends BaseJob {
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Address.fromExternal(context, content.getSender()),
message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000L, true,
+ 0,
content.isNeedsReceipt(),
Optional.absent(),
message.getGroupInfo(),
@@ -669,6 +676,17 @@ public class PushDecryptJob extends BaseJob {
MessageNotifier.updateNotification(context);
}
+ private void handleSynchronizeMessageTimerReadMessage(@NonNull MessageTimerReadMessage timerMessage, long envelopeTimestamp) {
+ SyncMessageId messageId = new SyncMessageId(Address.fromExternal(context, timerMessage.getSender()), timerMessage.getTimestamp());
+
+ DatabaseFactory.getMmsDatabase(context).markRevealStarted(messageId, envelopeTimestamp);
+ ApplicationContext.getInstance(context).getRevealableMessageManager().scheduleIfNecessary();
+
+ MessageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp);
+ MessageNotifier.cancelDelayedNotifications();
+ MessageNotifier.updateNotification(context);
+ }
+
private void handleMediaMessage(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message,
@NonNull Optional smsMessageId)
@@ -689,6 +707,7 @@ public class PushDecryptJob extends BaseJob {
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Address.fromExternal(context, content.getSender()),
message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000L, false,
+ message.getMessageTimerInSeconds() * 1000,
content.isNeedsReceipt(),
message.getBody(),
message.getGroupInfo(),
@@ -698,7 +717,6 @@ public class PushDecryptJob extends BaseJob {
linkPreviews,
sticker);
-
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
if (insertResult.isPresent()) {
@@ -728,6 +746,10 @@ public class PushDecryptJob extends BaseJob {
if (insertResult.isPresent()) {
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
+
+ if (message.getMessageTimerInSeconds() > 0) {
+ ApplicationContext.getInstance(context).getRevealableMessageManager().scheduleIfNecessary();
+ }
}
}
@@ -758,7 +780,8 @@ public class PushDecryptJob extends BaseJob {
Optional sticker = getStickerAttachment(message.getMessage().getSticker());
Optional> sharedContacts = getContacts(message.getMessage().getSharedContacts());
Optional> previews = getLinkPreviews(message.getMessage().getPreviews(), message.getMessage().getBody().or(""));
- List syncAttachments = PointerAttachment.forPointers(message.getMessage().getAttachments());
+ long messageTimer = message.getMessage().getMessageTimerInSeconds() * 1000;
+ List syncAttachments = messageTimer == 0 ? PointerAttachment.forPointers(message.getMessage().getAttachments()) : Collections.emptyList();
if (sticker.isPresent()) {
syncAttachments.add(sticker.get());
@@ -768,6 +791,7 @@ public class PushDecryptJob extends BaseJob {
syncAttachments,
message.getTimestamp(), -1,
message.getMessage().getExpiresInSeconds() * 1000,
+ messageTimer,
ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(),
sharedContacts.or(Collections.emptyList()),
previews.or(Collections.emptyList()),
@@ -897,6 +921,7 @@ public class PushDecryptJob extends BaseJob {
message.getTimestamp(), body,
message.getGroupInfo(),
message.getExpiresInSeconds() * 1000L,
+ message.getMessageTimerInSeconds() * 1000L,
content.isNeedsReceipt());
textMessage = new IncomingEncryptedMessage(textMessage, body);
@@ -931,7 +956,7 @@ public class PushDecryptJob extends BaseJob {
long messageId;
if (isGroup) {
- OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, new SlideDeck(), body, message.getTimestamp(), -1, expiresInMillis, ThreadDatabase.DistributionTypes.DEFAULT, null, Collections.emptyList(), Collections.emptyList());
+ OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, new SlideDeck(), body, message.getTimestamp(), -1, expiresInMillis, 0, ThreadDatabase.DistributionTypes.DEFAULT, null, Collections.emptyList(), Collections.emptyList());
outgoingMediaMessage = new OutgoingSecureMediaMessage(outgoingMediaMessage);
messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null);
@@ -1035,6 +1060,26 @@ public class PushDecryptJob extends BaseJob {
}
}
+ private void handleInvalidMessage(@NonNull String sender,
+ int senderDevice,
+ @NonNull Optional group,
+ long timestamp,
+ @NonNull Optional smsMessageId)
+ {
+ SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
+
+ if (!smsMessageId.isPresent()) {
+ Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp, group);
+
+ if (insertResult.isPresent()) {
+ smsDatabase.markAsInvalidMessage(insertResult.get().getMessageId());
+ MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
+ }
+ } else {
+ smsDatabase.markAsNoSession(smsMessageId.get());
+ }
+ }
+
private void handleLegacyMessage(@NonNull String sender, int senderDevice, long timestamp,
@NonNull Optional smsMessageId)
{
@@ -1149,6 +1194,17 @@ public class PushDecryptJob extends BaseJob {
}
}
+ private boolean isInvalidMessage(@NonNull SignalServiceDataMessage message) {
+ if (message.getMessageTimerInSeconds() > 0) {
+ return !message.getAttachments().isPresent() ||
+ message.getAttachments().get().size() != 1 ||
+ !MediaUtil.isImageType(message.getAttachments().get().get(0).getContentType().toLowerCase());
+
+ }
+
+ return false;
+ }
+
private Optional getValidatedQuote(Optional quote) {
if (!quote.isPresent()) return Optional.absent();
@@ -1172,12 +1228,18 @@ public class PushDecryptJob extends BaseJob {
if (message.isMms()) {
MmsMessageRecord mmsMessage = (MmsMessageRecord) message;
- attachments = mmsMessage.getSlideDeck().asAttachments();
- if (attachments.isEmpty()) {
- attachments.addAll(Stream.of(mmsMessage.getLinkPreviews())
- .filter(lp -> lp.getThumbnail().isPresent())
- .map(lp -> lp.getThumbnail().get())
- .toList());
+
+ if (mmsMessage.getRevealDuration() == 0) {
+ attachments = mmsMessage.getSlideDeck().asAttachments();
+
+ if (attachments.isEmpty()) {
+ attachments.addAll(Stream.of(mmsMessage.getLinkPreviews())
+ .filter(lp -> lp.getThumbnail().isPresent())
+ .map(lp -> lp.getThumbnail().get())
+ .toList());
+ }
+ } else if (quote.get().getAttachments().size() > 0) {
+ attachments.add(new TombstoneAttachment(quote.get().getAttachments().get(0).getContentType(), true));
}
}
@@ -1185,6 +1247,7 @@ public class PushDecryptJob extends BaseJob {
}
Log.w(TAG, "Didn't find matching message record...");
+
return Optional.of(new QuoteModel(quote.get().getId(),
author,
quote.get().getText(),
@@ -1272,7 +1335,7 @@ public class PushDecryptJob extends BaseJob {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
IncomingTextMessage textMessage = new IncomingTextMessage(Address.fromExternal(context, sender),
senderDevice, timestamp, "",
- group, 0, false);
+ group, 0, 0, false);
textMessage = new IncomingEncryptedMessage(textMessage, "");
return database.insertMessageInbox(textMessage);
diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
index 736475af1a..2a75ad07db 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
@@ -194,6 +194,13 @@ public class PushGroupSendJob extends PushSendJob {
.getExpiringMessageManager()
.scheduleDeletion(messageId, true, message.getExpiresIn());
}
+
+ if (message.getRevealDuration() > 0) {
+ database.markRevealStarted(messageId);
+ ApplicationContext.getInstance(context)
+ .getRevealableMessageManager()
+ .scheduleIfNecessary();
+ }
} else if (!networkFailures.isEmpty()) {
throw new RetryLaterException();
} else if (!identityMismatches.isEmpty()) {
@@ -262,6 +269,7 @@ public class PushGroupSendJob extends PushSendJob {
.withAttachments(attachmentPointers)
.withBody(message.getBody())
.withExpiration((int)(message.getExpiresIn() / 1000))
+ .withMessageTimer((int)(message.getRevealDuration() / 1000))
.asExpirationUpdate(message.isExpirationUpdate())
.withProfileKey(profileKey.orNull())
.withQuote(quote.orNull())
diff --git a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
index 0cf74c555d..40e026a122 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
@@ -159,6 +159,13 @@ public class PushMediaSendJob extends PushSendJob {
expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
}
+ if (message.getRevealDuration() > 0) {
+ database.markRevealStarted(messageId);
+ ApplicationContext.getInstance(context)
+ .getRevealableMessageManager()
+ .scheduleIfNecessary();
+ }
+
log(TAG, "Sent message: " + messageId);
} catch (InsecureFallbackApprovalException ifae) {
@@ -210,6 +217,7 @@ public class PushMediaSendJob extends PushSendJob {
.withAttachments(serviceAttachments)
.withTimestamp(message.getSentTimeMillis())
.withExpiration((int)(message.getExpiresIn() / 1000))
+ .withMessageTimer((int) message.getRevealDuration() / 1000)
.withProfileKey(profileKey.orNull())
.withQuote(quote.orNull())
.withSticker(sticker.orNull())
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
index 21341be3a9..5fbe15f241 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
@@ -47,7 +47,7 @@ import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
-import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.TimerState;
+import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.RevealState;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
@@ -122,6 +122,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
private ViewGroup composeContainer;
private ViewGroup countButton;
private TextView countButtonText;
+ private ImageView revealButton;
private EmojiEditText captionText;
private EmojiToggle emojiToggle;
private Stub emojiDrawer;
@@ -191,6 +192,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
composeContainer = findViewById(R.id.mediasend_compose_container);
countButton = findViewById(R.id.mediasend_count_button);
countButtonText = findViewById(R.id.mediasend_count_button_text);
+ revealButton = findViewById(R.id.mediasend_reveal_toggle);
captionText = findViewById(R.id.mediasend_caption);
emojiToggle = findViewById(R.id.mediasend_emoji_toggle);
charactersLeft = findViewById(R.id.mediasend_characters_left);
@@ -289,6 +291,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
.or(recipient.getAddress().serialize()));
composeText.setHint(getString(R.string.MediaSendActivity_message_to_s, displayName), null);
}
+
composeText.setOnEditorActionListener((v, actionId, event) -> {
boolean isSend = actionId == EditorInfo.IME_ACTION_SEND;
if (isSend) sendButton.performClick();
@@ -302,6 +305,8 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
}
initViewModel();
+
+ revealButton.setOnClickListener(v -> viewModel.onRevealButtonToggled());
}
@Override
@@ -512,14 +517,14 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
if (state == null) return;
hud.setVisibility(state.isHudVisible() ? View.VISIBLE : View.GONE);
- composeContainer.setVisibility(state.isComposeVisible() ? View.VISIBLE : (state.getTimerState() == TimerState.GONE ? View.GONE : View.INVISIBLE));
+ composeContainer.setVisibility(state.isComposeVisible() ? View.VISIBLE : (state.getRevealState() == RevealState.GONE ? View.GONE : View.INVISIBLE));
captionText.setVisibility(state.isCaptionVisible() ? View.VISIBLE : View.GONE);
int captionBackground;
if (state.getRailState() == MediaSendViewModel.RailState.VIEWABLE) {
captionBackground = R.color.core_grey_90;
- } else if (state.getTimerState() == TimerState.ENABLED) {
+ } else if (state.getRevealState() == RevealState.ENABLED) {
captionBackground = 0;
} else {
captionBackground = R.color.transparent_black_70;
@@ -543,6 +548,20 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
break;
}
+ switch (state.getRevealState()) {
+ case ENABLED:
+ revealButton.setVisibility(View.VISIBLE);
+ revealButton.setImageResource(R.drawable.ic_view_once_32);
+ break;
+ case DISABLED:
+ revealButton.setVisibility(View.VISIBLE);
+ revealButton.setImageResource(R.drawable.ic_view_infinite_32);
+ break;
+ case GONE:
+ revealButton.setVisibility(View.GONE);
+ break;
+ }
+
switch (state.getRailState()) {
case INTERACTIVE:
mediaRail.setVisibility(View.VISIBLE);
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
index 55d301f353..747cd04b7e 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
@@ -17,8 +17,10 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.revealable.RevealableUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -64,27 +66,27 @@ class MediaSendViewModel extends ViewModel {
private boolean captionVisible;
private ButtonState buttonState;
private RailState railState;
- private TimerState timerState;
+ private RevealState revealState;
private MediaSendViewModel(@NonNull Application application, @NonNull MediaRepository repository) {
- this.application = application;
- this.repository = repository;
- this.selectedMedia = new MutableLiveData<>();
- this.bucketMedia = new MutableLiveData<>();
- this.mostRecentMedia = new MutableLiveData<>();
- this.position = new MutableLiveData<>();
- this.bucketId = new MutableLiveData<>();
- this.folders = new MutableLiveData<>();
- this.hudState = new MutableLiveData<>();
- this.error = new SingleLiveEvent<>();
- this.savedDrawState = new HashMap<>();
- this.lastCameraCapture = Optional.absent();
- this.body = "";
- this.buttonState = ButtonState.GONE;
- this.railState = RailState.GONE;
- this.timerState = TimerState.GONE;
- this.page = Page.UNKNOWN;
+ this.application = application;
+ this.repository = repository;
+ this.selectedMedia = new MutableLiveData<>();
+ this.bucketMedia = new MutableLiveData<>();
+ this.mostRecentMedia = new MutableLiveData<>();
+ this.position = new MutableLiveData<>();
+ this.bucketId = new MutableLiveData<>();
+ this.folders = new MutableLiveData<>();
+ this.hudState = new MutableLiveData<>();
+ this.error = new SingleLiveEvent<>();
+ this.savedDrawState = new HashMap<>();
+ this.lastCameraCapture = Optional.absent();
+ this.body = "";
+ this.buttonState = ButtonState.GONE;
+ this.railState = RailState.GONE;
+ this.revealState = RevealState.GONE;
+ this.page = Page.UNKNOWN;
position.setValue(-1);
}
@@ -171,7 +173,7 @@ class MediaSendViewModel extends ViewModel {
captionVisible = false;
buttonState = ButtonState.COUNT;
railState = RailState.VIEWABLE;
- timerState = TimerState.GONE;
+ revealState = RevealState.GONE;
hudState.setValue(buildHudState());
}
@@ -179,20 +181,28 @@ class MediaSendViewModel extends ViewModel {
void onImageEditorStarted() {
page = Page.EDITOR;
hudVisible = true;
- composeVisible = timerState != TimerState.ENABLED;
+ composeVisible = revealState != RevealState.ENABLED;
captionVisible = getSelectedMediaOrDefault().size() > 1 || (getSelectedMediaOrDefault().size() > 0 && getSelectedMediaOrDefault().get(0).getCaption().isPresent());
buttonState = ButtonState.SEND;
- railState = !isSms ? RailState.INTERACTIVE : RailState.GONE;
+
+ if (revealState == RevealState.GONE && revealSupported()) {
+ revealState = TextSecurePreferences.isRevealableMessageEnabled(application) ? RevealState.ENABLED : RevealState.DISABLED;
+ } else if (!revealSupported()) {
+ revealState = RevealState.GONE;
+ }
+
+ railState = !isSms && revealState != RevealState.ENABLED ? RailState.INTERACTIVE : RailState.GONE;
hudState.setValue(buildHudState());
}
void onCameraStarted() {
+ // TODO: Don't need this?
Page previous = page;
page = Page.CAMERA;
hudVisible = false;
- timerState = TimerState.GONE;
+ revealState = RevealState.GONE;
buttonState = ButtonState.COUNT;
List selected = getSelectedMediaOrDefault();
@@ -212,7 +222,7 @@ class MediaSendViewModel extends ViewModel {
composeVisible = false;
captionVisible = false;
buttonState = ButtonState.COUNT;
- timerState = TimerState.GONE;
+ revealState = RevealState.GONE;
railState = getSelectedMediaOrDefault().isEmpty() ? RailState.GONE : RailState.VIEWABLE;
lastCameraCapture = Optional.absent();
@@ -226,7 +236,7 @@ class MediaSendViewModel extends ViewModel {
composeVisible = false;
captionVisible = false;
buttonState = ButtonState.COUNT;
- timerState = TimerState.GONE;
+ revealState = RevealState.GONE;
railState = getSelectedMediaOrDefault().isEmpty() ? RailState.GONE : RailState.VIEWABLE;
lastCameraCapture = Optional.absent();
@@ -234,10 +244,20 @@ class MediaSendViewModel extends ViewModel {
hudState.setValue(buildHudState());
}
- void onTimerButtonToggled() {
+ void onRevealButtonToggled() {
hudVisible = true;
- timerState = (timerState == TimerState.ENABLED) ? TimerState.DISABLED : TimerState.ENABLED;
- composeVisible = (timerState != TimerState.ENABLED);
+ revealState = revealState == RevealState.ENABLED ? RevealState.DISABLED : RevealState.ENABLED;
+ composeVisible = revealState != RevealState.ENABLED;
+ railState = revealState == RevealState.ENABLED || isSms ? RailState.GONE : RailState.INTERACTIVE;
+ captionVisible = false;
+
+ List uncaptioned = Stream.of(getSelectedMediaOrDefault())
+ .map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getBucketId(), Optional.absent()))
+ .toList();
+
+ selectedMedia.setValue(uncaptioned);
+
+ TextSecurePreferences.setIsRevealableMessageEnabled(application, revealState == RevealState.ENABLED);
hudState.setValue(buildHudState());
}
@@ -245,14 +265,14 @@ class MediaSendViewModel extends ViewModel {
void onKeyboardHidden(boolean isSms) {
if (page != Page.EDITOR) return;
- composeVisible = (timerState != TimerState.ENABLED);
+ composeVisible = (revealState != RevealState.ENABLED);
buttonState = ButtonState.SEND;
if (isSms) {
railState = RailState.GONE;
captionVisible = false;
} else {
- railState = RailState.INTERACTIVE;
+ railState = revealState != RevealState.ENABLED ? RailState.INTERACTIVE : RailState.GONE;
if (getSelectedMediaOrDefault().size() > 1 || (getSelectedMediaOrDefault().size() > 0 && getSelectedMediaOrDefault().get(0).getCaption().isPresent())) {
captionVisible = true;
@@ -267,18 +287,18 @@ class MediaSendViewModel extends ViewModel {
if (isSms) {
railState = RailState.GONE;
- composeVisible = (timerState == TimerState.GONE);
+ composeVisible = (revealState == RevealState.GONE);
captionVisible = false;
buttonState = ButtonState.SEND;
} else {
if (isCaptionFocused) {
- railState = RailState.INTERACTIVE;
+ railState = revealState != RevealState.ENABLED ? RailState.INTERACTIVE : RailState.GONE;
composeVisible = false;
captionVisible = true;
buttonState = ButtonState.GONE;
} else if (isComposeFocused) {
- railState = RailState.INTERACTIVE;
- composeVisible = (timerState != TimerState.ENABLED);
+ railState = revealState != RevealState.ENABLED ? RailState.INTERACTIVE : RailState.GONE;
+ composeVisible = (revealState != RevealState.ENABLED);
captionVisible = false;
buttonState = ButtonState.SEND;
}
@@ -327,6 +347,10 @@ class MediaSendViewModel extends ViewModel {
this.position.setValue(Math.min(position, getSelectedMediaOrDefault().size() - 1));
}
+ if (getSelectedMediaOrDefault().size() == 1) {
+ revealState = revealSupported() ? RevealState.DISABLED : RevealState.GONE;
+ }
+
hudState.setValue(buildHudState());
}
@@ -350,16 +374,6 @@ class MediaSendViewModel extends ViewModel {
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID);
}
- void onImageCaptureUndo(@NonNull Context context) {
- List selected = getSelectedMediaOrDefault();
-
- if (lastCameraCapture.isPresent() && selected.contains(lastCameraCapture.get()) && selected.size() == 1) {
- selected.remove(lastCameraCapture.get());
- selectedMedia.setValue(selected);
- BlobProvider.getInstance().delete(context, lastCameraCapture.get().getUri());
- }
- }
-
void onCaptionChanged(@NonNull String newCaption) {
if (position.getValue() >= 0 && !Util.isEmpty(selectedMedia.getValue())) {
selectedMedia.getValue().get(position.getValue()).setCaption(TextUtils.isEmpty(newCaption) ? null : newCaption);
@@ -426,6 +440,8 @@ class MediaSendViewModel extends ViewModel {
}
long getRevealDuration() {
+ // TODO[reveal]
+// return revealState == RevealState.ENABLED ? RevealableUtil.DURATION : 0;
return 0;
}
@@ -447,12 +463,14 @@ class MediaSendViewModel extends ViewModel {
}
private HudState buildHudState() {
- List selectedMedia = getSelectedMediaOrDefault();
- int selectionCount = selectedMedia.size();
- ButtonState updatedButtonState = buttonState == ButtonState.COUNT && selectionCount == 0 ? ButtonState.GONE : buttonState;
- boolean updatdCaptionVisible = captionVisible && (selectedMedia.size() > 1 || (selectedMedia.size() > 0 && selectedMedia.get(0).getCaption().isPresent()));
+ // TODO[reveal]
+ RevealState updatedRevealState = RevealState.GONE;
+ List selectedMedia = getSelectedMediaOrDefault();
+ int selectionCount = selectedMedia.size();
+ ButtonState updatedButtonState = buttonState == ButtonState.COUNT && selectionCount == 0 ? ButtonState.GONE : buttonState;
+ boolean updatedCaptionVisible = captionVisible && (selectedMedia.size() > 1 || (selectedMedia.size() > 0 && selectedMedia.get(0).getCaption().isPresent()));
- return new HudState(hudVisible, composeVisible, updatdCaptionVisible, selectionCount, updatedButtonState, railState, timerState);
+ return new HudState(hudVisible, composeVisible, updatedCaptionVisible, selectionCount, updatedButtonState, railState, updatedRevealState);
}
private void clearPersistedMedia() {
@@ -462,6 +480,14 @@ class MediaSendViewModel extends ViewModel {
.forEach(uri -> BlobProvider.getInstance().delete(application.getApplicationContext(), uri));
}
+ private boolean revealSupported() {
+ return !isSms && mediaSupportsRevealableMessage(getSelectedMediaOrDefault());
+ }
+
+ private boolean mediaSupportsRevealableMessage(@NonNull List media) {
+ return media.size() == 1 && MediaUtil.isImageType(media.get(0).getMimeType());
+ }
+
@Override
protected void onCleared() {
if (!sentMedia) {
@@ -485,7 +511,7 @@ class MediaSendViewModel extends ViewModel {
INTERACTIVE, VIEWABLE, GONE
}
- enum TimerState {
+ enum RevealState {
ENABLED, DISABLED, GONE
}
@@ -497,7 +523,7 @@ class MediaSendViewModel extends ViewModel {
private final int selectionCount;
private final ButtonState buttonState;
private final RailState railState;
- private final TimerState timerState;
+ private final RevealState revealState;
HudState(boolean hudVisible,
boolean composeVisible,
@@ -505,7 +531,7 @@ class MediaSendViewModel extends ViewModel {
int selectionCount,
@NonNull ButtonState buttonState,
@NonNull RailState railState,
- @NonNull TimerState timerState)
+ @NonNull RevealState revealState)
{
this.hudVisible = hudVisible;
this.composeVisible = composeVisible;
@@ -513,7 +539,7 @@ class MediaSendViewModel extends ViewModel {
this.selectionCount = selectionCount;
this.buttonState = buttonState;
this.railState = railState;
- this.timerState = timerState;
+ this.revealState = revealState;
}
public boolean isHudVisible() {
@@ -540,8 +566,9 @@ class MediaSendViewModel extends ViewModel {
return hudVisible ? railState : RailState.GONE;
}
- public @NonNull TimerState getTimerState() {
- return hudVisible ? timerState : TimerState.GONE;
+ public @NonNull
+ RevealState getRevealState() {
+ return hudVisible ? revealState : RevealState.GONE;
}
}
diff --git a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java
index 8d0c19962c..5c85f0c072 100644
--- a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java
+++ b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java
@@ -24,6 +24,7 @@ public class IncomingMediaMessage {
private final int subscriptionId;
private final long expiresIn;
private final boolean expirationUpdate;
+ private final long revealDuration;
private final QuoteModel quote;
private final boolean unidentified;
@@ -39,6 +40,7 @@ public class IncomingMediaMessage {
int subscriptionId,
long expiresIn,
boolean expirationUpdate,
+ long revealDuration,
boolean unidentified)
{
this.from = from;
@@ -49,6 +51,7 @@ public class IncomingMediaMessage {
this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
this.expirationUpdate = expirationUpdate;
+ this.revealDuration = revealDuration;
this.quote = null;
this.unidentified = unidentified;
@@ -60,6 +63,7 @@ public class IncomingMediaMessage {
int subscriptionId,
long expiresIn,
boolean expirationUpdate,
+ long revealDuration,
boolean unidentified,
Optional body,
Optional group,
@@ -76,6 +80,7 @@ public class IncomingMediaMessage {
this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
this.expirationUpdate = expirationUpdate;
+ this.revealDuration = revealDuration;
this.quote = quote.orNull();
this.unidentified = unidentified;
@@ -127,6 +132,10 @@ public class IncomingMediaMessage {
return expiresIn;
}
+ public long getRevealDuration() {
+ return revealDuration;
+ }
+
public boolean isGroupMessage() {
return groupId != null;
}
diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java
index 3951cb0aab..bd521a7310 100644
--- a/src/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java
+++ b/src/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java
@@ -11,7 +11,7 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) {
super(recipient, "", new LinkedList(), sentTimeMillis,
- ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList(),
+ ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, 0, null, Collections.emptyList(),
Collections.emptyList());
}
diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java
index 27ad272116..36d567acfa 100644
--- a/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java
+++ b/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java
@@ -24,13 +24,14 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
@NonNull List avatar,
long sentTimeMillis,
long expiresIn,
+ long revealDuration,
@Nullable QuoteModel quote,
@NonNull List contacts,
@NonNull List previews)
throws IOException
{
super(recipient, encodedGroupContext, avatar, sentTimeMillis,
- ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, quote, contacts, previews);
+ ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, revealDuration, quote, contacts, previews);
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
}
@@ -40,6 +41,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
@Nullable final Attachment avatar,
long sentTimeMillis,
long expireIn,
+ long revealDuration,
@Nullable QuoteModel quote,
@NonNull List contacts,
@NonNull List previews)
@@ -47,7 +49,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
super(recipient, Base64.encodeBytes(group.toByteArray()),
new LinkedList() {{if (avatar != null) add(avatar);}},
System.currentTimeMillis(),
- ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
+ ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, revealDuration, quote, contacts, previews);
this.group = group;
}
diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java
index 6a73726448..cc36527bc4 100644
--- a/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java
+++ b/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java
@@ -23,6 +23,7 @@ public class OutgoingMediaMessage {
private final int distributionType;
private final int subscriptionId;
private final long expiresIn;
+ private final long revealDuration;
private final QuoteModel outgoingQuote;
private final List networkFailures = new LinkedList<>();
@@ -32,7 +33,7 @@ public class OutgoingMediaMessage {
public OutgoingMediaMessage(Recipient recipient, String message,
List attachments, long sentTimeMillis,
- int subscriptionId, long expiresIn,
+ int subscriptionId, long expiresIn, long revealDuration,
int distributionType,
@Nullable QuoteModel outgoingQuote,
@NonNull List contacts,
@@ -47,6 +48,7 @@ public class OutgoingMediaMessage {
this.attachments = attachments;
this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
+ this.revealDuration = revealDuration;
this.outgoingQuote = outgoingQuote;
this.contacts.addAll(contacts);
@@ -57,7 +59,8 @@ public class OutgoingMediaMessage {
public OutgoingMediaMessage(Recipient recipient, SlideDeck slideDeck, String message,
long sentTimeMillis, int subscriptionId, long expiresIn,
- int distributionType, @Nullable QuoteModel outgoingQuote,
+ long revealDuration, int distributionType,
+ @Nullable QuoteModel outgoingQuote,
@NonNull List contacts,
@NonNull List linkPreviews)
{
@@ -65,7 +68,7 @@ public class OutgoingMediaMessage {
buildMessage(slideDeck, message),
slideDeck.asAttachments(),
sentTimeMillis, subscriptionId,
- expiresIn, distributionType, outgoingQuote,
+ expiresIn, revealDuration, distributionType, outgoingQuote,
contacts, linkPreviews, new LinkedList<>(), new LinkedList<>());
}
@@ -77,6 +80,7 @@ public class OutgoingMediaMessage {
this.sentTimeMillis = that.sentTimeMillis;
this.subscriptionId = that.subscriptionId;
this.expiresIn = that.expiresIn;
+ this.revealDuration = that.revealDuration;
this.outgoingQuote = that.outgoingQuote;
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
@@ -125,6 +129,10 @@ public class OutgoingMediaMessage {
return expiresIn;
}
+ public long getRevealDuration() {
+ return revealDuration;
+ }
+
public @Nullable QuoteModel getOutgoingQuote() {
return outgoingQuote;
}
diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java
index 8332c06575..839b26ce44 100644
--- a/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java
+++ b/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java
@@ -18,11 +18,12 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
long sentTimeMillis,
int distributionType,
long expiresIn,
+ long revealDuration,
@Nullable QuoteModel quote,
@NonNull List contacts,
@NonNull List previews)
{
- super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
+ super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, revealDuration, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
}
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
diff --git a/src/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java b/src/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java
index 31d704f9b2..09c9ed8c69 100644
--- a/src/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java
+++ b/src/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java
@@ -76,7 +76,7 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver {
if (recipient.isGroupRecipient()) {
Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message");
- OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
+ OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
replyThreadId = MessageSender.send(context, reply, threadId, false, null);
} else {
Log.w("AndroidAutoReplyReceiver", "Sending regular message ");
diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java
index 9dd2aaff27..88f98c389d 100644
--- a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java
+++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java
@@ -465,6 +465,9 @@ public class MessageNotifier {
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_sticker));
slideDeck = ((MmsMessageRecord) record).getSlideDeck();
+ } else if (record.isMms() && ((MmsMessageRecord) record).getRevealDuration() > 0) {
+ body = SpanUtil.italic(context.getString(R.string.MessageNotifier_disappearing_photo));
+ slideDeck = ((MmsMessageRecord) record).getSlideDeck();
} else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message));
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
diff --git a/src/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java b/src/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java
index 3cd7789c68..7276d03b02 100644
--- a/src/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java
+++ b/src/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java
@@ -76,7 +76,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
switch (replyMethod) {
case GroupMessage: {
- OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
+ OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
threadId = MessageSender.send(context, reply, -1, false, null);
break;
}
diff --git a/src/org/thoughtcrime/securesms/revealable/RevealExpirationInfo.java b/src/org/thoughtcrime/securesms/revealable/RevealExpirationInfo.java
new file mode 100644
index 0000000000..eb5e07f097
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/revealable/RevealExpirationInfo.java
@@ -0,0 +1,32 @@
+package org.thoughtcrime.securesms.revealable;
+
+public class RevealExpirationInfo {
+
+ private final long messageId;
+ private final long receiveTime;
+ private final long revealStartTime;
+ private final long revealDuration;
+
+ public RevealExpirationInfo(long messageId, long receiveTime, long revealStartTime, long revealDuration) {
+ this.messageId = messageId;
+ this.receiveTime = receiveTime;
+ this.revealStartTime = revealStartTime;
+ this.revealDuration = revealDuration;
+ }
+
+ public long getMessageId() {
+ return messageId;
+ }
+
+ public long getReceiveTime() {
+ return receiveTime;
+ }
+
+ public long getRevealStartTime() {
+ return revealStartTime;
+ }
+
+ public long getRevealDuration() {
+ return revealDuration;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/revealable/RevealableMessageActivity.java b/src/org/thoughtcrime/securesms/revealable/RevealableMessageActivity.java
new file mode 100644
index 0000000000..e05b5701a7
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/revealable/RevealableMessageActivity.java
@@ -0,0 +1,71 @@
+package org.thoughtcrime.securesms.revealable;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.pnikosis.materialishprogress.ProgressWheel;
+
+import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
+import org.thoughtcrime.securesms.mms.GlideApp;
+import org.thoughtcrime.securesms.util.Util;
+
+public class RevealableMessageActivity extends PassphraseRequiredActionBarActivity {
+
+ private static final String TAG = Log.tag(RevealableMessageActivity.class);
+
+ private static final String KEY_MESSAGE_ID = "message_id";
+
+ private ImageView image;
+ private View closeButton;
+ private RevealableMessageViewModel viewModel;
+
+ public static Intent getIntent(@NonNull Context context, long messageId) {
+ Intent intent = new Intent(context, RevealableMessageActivity.class);
+ intent.putExtra(KEY_MESSAGE_ID, messageId);
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState, boolean ready) {
+ super.onCreate(savedInstanceState, ready);
+ setContentView(R.layout.revealable_message_activity);
+
+ this.image = findViewById(R.id.reveal_image);
+ this.closeButton = findViewById(R.id.reveal_close_button);
+
+ image.setOnClickListener(v -> finish());
+ closeButton.setOnClickListener(v -> finish());
+
+ initViewModel(getIntent().getLongExtra(KEY_MESSAGE_ID, -1));
+ }
+
+ private void initViewModel(long messageId) {
+ RevealableMessageRepository repository = new RevealableMessageRepository(this);
+ viewModel = ViewModelProviders.of(this, new RevealableMessageViewModel.Factory(getApplication(), messageId, repository))
+ .get(RevealableMessageViewModel.class);
+
+ viewModel.getMessage().observe(this, (message) -> {
+ if (message == null) return;
+
+ if (message.isPresent()) {
+ //noinspection ConstantConditions
+ GlideApp.with(this)
+ .load(new DecryptableUri(message.get().getSlideDeck().getThumbnailSlide().getUri()))
+ .into(image);
+ } else {
+ image.setImageDrawable(null);
+ finish();
+ }
+ });
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/revealable/RevealableMessageManager.java b/src/org/thoughtcrime/securesms/revealable/RevealableMessageManager.java
new file mode 100644
index 0000000000..9d7df2f8ba
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/revealable/RevealableMessageManager.java
@@ -0,0 +1,87 @@
+package org.thoughtcrime.securesms.revealable;
+
+import android.app.Application;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import org.thoughtcrime.securesms.ApplicationContext;
+import org.thoughtcrime.securesms.database.AttachmentDatabase;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.MmsDatabase;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.service.TimedEventManager;
+
+/**
+ * Manages clearing removable message content after they're opened.
+ */
+public class RevealableMessageManager extends TimedEventManager {
+
+ private static final String TAG = Log.tag(RevealableMessageManager.class);
+
+ private final MmsDatabase mmsDatabase;
+ private final AttachmentDatabase attachmentDatabase;
+
+ public RevealableMessageManager(@NonNull Application application) {
+ super(application, "RevealableMessageManager");
+
+ this.mmsDatabase = DatabaseFactory.getMmsDatabase(application);
+ this.attachmentDatabase = DatabaseFactory.getAttachmentDatabase(application);
+ }
+
+ @WorkerThread
+ @Override
+ protected @Nullable RevealExpirationInfo getNextClosestEvent() {
+ RevealExpirationInfo expirationInfo = mmsDatabase.getNearestExpiringRevealableMessage();
+
+ if (expirationInfo != null) {
+ Log.i(TAG, "Next closest expiration is in " + getDelayForEvent(expirationInfo) + " ms for messsage " + expirationInfo.getMessageId() + ".");
+ } else {
+ Log.i(TAG, "No messages to schedule.");
+ }
+
+ return expirationInfo;
+ }
+
+ @WorkerThread
+ @Override
+ protected void executeEvent(@NonNull RevealExpirationInfo event) {
+ Log.i(TAG, "Deleting attachments for message " + event.getMessageId());
+ attachmentDatabase.deleteAttachmentFilesForMessage(event.getMessageId());
+ }
+
+ @WorkerThread
+ @Override
+ protected long getDelayForEvent(@NonNull RevealExpirationInfo event) {
+ if (event.getRevealStartTime() == 0) {
+ return event.getReceiveTime() + RevealableUtil.MAX_LIFESPAN;
+ } else {
+ long timeSinceStart = System.currentTimeMillis() - event.getRevealStartTime();
+ long timeLeft = event.getRevealDuration() - timeSinceStart;
+
+ return Math.max(0, timeLeft);
+ }
+ }
+
+ @AnyThread
+ @Override
+ protected void scheduleAlarm(@NonNull Application application, long delay) {
+ setAlarm(application, delay, RevealAlarm.class);
+ }
+
+ public static class RevealAlarm extends BroadcastReceiver {
+
+ private static final String TAG = Log.tag(RevealAlarm.class);
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "onReceive()");
+ ApplicationContext.getInstance(context).getRevealableMessageManager().scheduleIfNecessary();
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/revealable/RevealableMessageRepository.java b/src/org/thoughtcrime/securesms/revealable/RevealableMessageRepository.java
new file mode 100644
index 0000000000..dbce55a718
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/revealable/RevealableMessageRepository.java
@@ -0,0 +1,37 @@
+package org.thoughtcrime.securesms.revealable;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.database.AttachmentDatabase;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.MmsDatabase;
+import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+class RevealableMessageRepository {
+
+ private static final String TAG = Log.tag(RevealableMessageRepository.class);
+
+ private final MmsDatabase mmsDatabase;
+
+ RevealableMessageRepository(@NonNull Context context) {
+ this.mmsDatabase = DatabaseFactory.getMmsDatabase(context);
+ }
+
+ void getMessage(long messageId, @NonNull Callback> callback) {
+ SignalExecutors.BOUNDED.execute(() -> {
+ try (MmsDatabase.Reader reader = mmsDatabase.readerFor(mmsDatabase.getMessage(messageId))) {
+ MmsMessageRecord record = (MmsMessageRecord) reader.getNext();
+ callback.onComplete(Optional.fromNullable(record));
+ }
+ });
+ }
+
+ interface Callback {
+ void onComplete(T result);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/revealable/RevealableMessageView.java b/src/org/thoughtcrime/securesms/revealable/RevealableMessageView.java
new file mode 100644
index 0000000000..03c116dbd9
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/revealable/RevealableMessageView.java
@@ -0,0 +1,174 @@
+package org.thoughtcrime.securesms.revealable;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.pnikosis.materialishprogress.ProgressWheel;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.attachments.Attachment;
+import org.thoughtcrime.securesms.database.AttachmentDatabase;
+import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
+import org.thoughtcrime.securesms.events.PartProgressEvent;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.util.Util;
+
+public class RevealableMessageView extends LinearLayout {
+
+ private static final String TAG = Log.tag(RevealableMessageView.class);
+
+ private ImageView icon;
+ private ProgressWheel progress;
+ private TextView text;
+ private Handler handler;
+ private Runnable updateRunnable;
+ private Attachment attachment;
+ private int unopenedForegroundColor;
+ private int openedForegroundColor;
+ private int foregroundColor;
+
+ public RevealableMessageView(Context context) {
+ super(context);
+ init(null);
+ }
+
+ public RevealableMessageView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init(attrs);
+ }
+
+ private void init(@Nullable AttributeSet attrs) {
+ inflate(getContext(), R.layout.revealable_message_view, this);
+ setOrientation(LinearLayout.HORIZONTAL);
+
+ if (attrs != null) {
+ TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.RevealableMessageView, 0, 0);
+
+ unopenedForegroundColor = typedArray.getColor(R.styleable.RevealableMessageView_revealable_unopenedForegroundColor, Color.BLACK);
+ openedForegroundColor = typedArray.getColor(R.styleable.RevealableMessageView_revealable_openedForegroundColor, Color.BLACK);
+
+ typedArray.recycle();
+ }
+
+ this.icon = findViewById(R.id.revealable_icon);
+ this.progress = findViewById(R.id.revealable_progress);
+ this.text = findViewById(R.id.revealable_text);
+ this.handler = new Handler();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (!EventBus.getDefault().isRegistered(this)) {
+ EventBus.getDefault().register(this);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ EventBus.getDefault().unregister(this);
+ }
+
+ public boolean requiresTapToDownload(@NonNull MmsMessageRecord messageRecord) {
+ if (messageRecord.isOutgoing() || messageRecord.getSlideDeck().getThumbnailSlide() == null) {
+ return false;
+ }
+
+ Attachment attachment = messageRecord.getSlideDeck().getThumbnailSlide().asAttachment();
+ return attachment.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_FAILED ||
+ attachment.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING;
+ }
+
+ public void setMessage(@NonNull MmsMessageRecord message) {
+ this.attachment = message.getSlideDeck().getThumbnailSlide() != null ? message.getSlideDeck().getThumbnailSlide().asAttachment() : null;
+
+ clearUpdateRunnable();
+ presentMessage(message);
+ }
+
+ public void presentMessage(@NonNull MmsMessageRecord message) {
+ presentText(message);
+ }
+
+ private void presentText(@NonNull MmsMessageRecord messageRecord) {
+ if (downloadInProgress(messageRecord) && messageRecord.isOutgoing()) {
+ foregroundColor = unopenedForegroundColor;
+ text.setText(R.string.RevealableMessageView_view_photo);
+ icon.setImageResource(0);
+ progress.setVisibility(VISIBLE);
+ } else if (downloadInProgress(messageRecord)) {
+ foregroundColor = unopenedForegroundColor;
+ text.setText("");
+ icon.setImageResource(0);
+ progress.setVisibility(VISIBLE);
+ } else if (requiresTapToDownload(messageRecord)) {
+ foregroundColor = unopenedForegroundColor;
+ text.setText(formatFileSize(messageRecord));
+ icon.setImageResource(R.drawable.ic_arrow_down_circle_outline_24);
+ progress.setVisibility(GONE);
+ } else if (RevealableUtil.isViewable(messageRecord)) {
+ foregroundColor = unopenedForegroundColor;
+ text.setText(R.string.RevealableMessageView_view_photo);
+ icon.setImageResource(R.drawable.ic_play_solid_24);
+ progress.setVisibility(GONE);
+ } else if (messageRecord.isOutgoing()) {
+ foregroundColor = openedForegroundColor;
+ text.setText(R.string.RevealableMessageView_photo);
+ icon.setImageResource(R.drawable.ic_play_outline_24);
+ progress.setVisibility(GONE);
+ } else {
+ foregroundColor = openedForegroundColor;
+ text.setText(R.string.RevealableMessageView_viewed);
+ icon.setImageResource(R.drawable.ic_play_outline_24);
+ progress.setVisibility(GONE);
+ clearUpdateRunnable();
+ }
+
+ text.setTextColor(foregroundColor);
+ icon.setColorFilter(foregroundColor);
+ progress.setBarColor(foregroundColor);
+ progress.setRimColor(Color.TRANSPARENT);
+ }
+
+ private boolean downloadInProgress(@NonNull MmsMessageRecord messageRecord) {
+ if (messageRecord.getSlideDeck().getThumbnailSlide() == null) return false;
+
+ Attachment attachment = messageRecord.getSlideDeck().getThumbnailSlide().asAttachment();
+ return attachment.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED;
+ }
+
+ private void clearUpdateRunnable() {
+ if (updateRunnable != null) {
+ handler.removeCallbacks(updateRunnable);
+ updateRunnable = null;
+ }
+ }
+
+ private @NonNull String formatFileSize(@NonNull MmsMessageRecord messageRecord) {
+ if (messageRecord.getSlideDeck().getThumbnailSlide() == null) return "";
+
+ long size = messageRecord.getSlideDeck().getThumbnailSlide().getFileSize();
+ return Util.getPrettyFileSize(size);
+ }
+
+ @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
+ public void onEventAsync(final PartProgressEvent event) {
+ if (event.attachment.equals(attachment)) {
+ progress.setInstantProgress((float) event.progress / (float) event.total);
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/revealable/RevealableMessageViewModel.java b/src/org/thoughtcrime/securesms/revealable/RevealableMessageViewModel.java
new file mode 100644
index 0000000000..7993d9b725
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/revealable/RevealableMessageViewModel.java
@@ -0,0 +1,96 @@
+package org.thoughtcrime.securesms.revealable;
+
+import android.app.Application;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import org.thoughtcrime.securesms.database.DatabaseContentProviders;
+import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.util.Util;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+class RevealableMessageViewModel extends ViewModel {
+
+ private static final String TAG = Log.tag(RevealableMessageViewModel.class);
+
+ private final Application application;
+ private final RevealableMessageRepository repository;
+ private final MutableLiveData> message;
+ private final ContentObserver observer;
+
+ private RevealableMessageViewModel(@NonNull Application application,
+ long messageId,
+ @NonNull RevealableMessageRepository repository)
+ {
+ this.application = application;
+ this.repository = repository;
+ this.message = new MutableLiveData<>();
+ this.observer = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ repository.getMessage(messageId, optionalMessage -> onMessageRetrieved(optionalMessage));
+ }
+ };
+
+ repository.getMessage(messageId, message -> {
+ if (message.isPresent()) {
+ Uri uri = DatabaseContentProviders.Conversation.getUriForThread(message.get().getThreadId());
+ application.getContentResolver().registerContentObserver(uri, true, observer);
+ }
+
+ onMessageRetrieved(message);
+ });
+ }
+
+ @NonNull LiveData> getMessage() {
+ return message;
+ }
+
+ @Override
+ protected void onCleared() {
+ application.getContentResolver().unregisterContentObserver(observer);
+ }
+
+ private void onMessageRetrieved(@NonNull Optional optionalMessage) {
+ Util.runOnMain(() -> {
+ MmsMessageRecord current = message.getValue() != null ? message.getValue().orNull() : null;
+ MmsMessageRecord proposed = optionalMessage.orNull();
+
+ if (current != null && proposed != null && current.getId() == proposed.getId()) {
+ Log.d(TAG, "Same ID -- skipping update");
+ } else {
+ message.setValue(optionalMessage);
+ }
+ });
+ }
+
+ static class Factory extends ViewModelProvider.NewInstanceFactory {
+
+ private final Application application;
+ private final long messageId;
+ private final RevealableMessageRepository repository;
+
+ Factory(@NonNull Application application,
+ long messageId,
+ @NonNull RevealableMessageRepository repository)
+ {
+ this.application = application;
+ this.messageId = messageId;
+ this.repository = repository;
+ }
+
+ @Override
+ public @NonNull T create(@NonNull Class modelClass) {
+ //noinspection ConstantConditions
+ return modelClass.cast(new RevealableMessageViewModel(application, messageId, repository));
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/revealable/RevealableUtil.java b/src/org/thoughtcrime/securesms/revealable/RevealableUtil.java
new file mode 100644
index 0000000000..e1c3f06d43
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/revealable/RevealableUtil.java
@@ -0,0 +1,61 @@
+package org.thoughtcrime.securesms.revealable;
+
+import androidx.annotation.Nullable;
+
+import org.thoughtcrime.securesms.attachments.Attachment;
+import org.thoughtcrime.securesms.database.AttachmentDatabase;
+import org.thoughtcrime.securesms.database.model.MessageRecord;
+import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
+
+import java.util.concurrent.TimeUnit;
+
+public class RevealableUtil {
+
+ public static final long MAX_LIFESPAN = TimeUnit.DAYS.toMillis(30);
+ public static final long DURATION = TimeUnit.SECONDS.toMillis(5);
+
+ public static boolean isViewable(@Nullable MmsMessageRecord message) {
+ if (message.getRevealDuration() == 0) {
+ return true;
+ } else if (message.getSlideDeck().getThumbnailSlide() == null) {
+ return false;
+ } else if (message.getSlideDeck().getThumbnailSlide().getUri() == null) {
+ return false;
+ } else if (message.isOutgoing() && message.getSlideDeck().getThumbnailSlide().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
+ return true;
+ } else if (message.getSlideDeck().getThumbnailSlide().getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
+ return false;
+ } else if (isRevealExpired(message)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ public static boolean isRevealExpired(@Nullable MmsMessageRecord message) {
+ if (message == null) {
+ return false;
+ } else if (message.getRevealDuration() == 0) {
+ return false;
+ } else if (message.getDateReceived() + MAX_LIFESPAN < System.currentTimeMillis()) {
+ return true;
+ } else if (message.getRevealStartTime() == 0) {
+ return false;
+ } else if (message.getRevealStartTime() + message.getRevealDuration() < System.currentTimeMillis()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public static boolean hasStarted(@Nullable MmsMessageRecord record) {
+ return record != null && record.getRevealStartTime() != 0;
+ }
+
+ public static boolean hasMedia(@Nullable MmsMessageRecord record) {
+ return record != null &&
+ record.getSlideDeck().getThumbnailSlide() != null &&
+ record.getSlideDeck().getThumbnailSlide().getUri() != null &&
+ record.getSlideDeck().getThumbnailSlide().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/service/TimedEventManager.java b/src/org/thoughtcrime/securesms/service/TimedEventManager.java
new file mode 100644
index 0000000000..5cbbdf0a79
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/service/TimedEventManager.java
@@ -0,0 +1,95 @@
+package org.thoughtcrime.securesms.service;
+
+import android.app.AlarmManager;
+import android.app.Application;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import org.thoughtcrime.securesms.util.ServiceUtil;
+
+/**
+ * Class to help manage scheduling events to happen in the future, whether the app is open or not.
+ */
+public abstract class TimedEventManager {
+
+ private final Application application;
+ private final Handler handler;
+
+ public TimedEventManager(@NonNull Application application, @NonNull String threadName) {
+ HandlerThread handlerThread = new HandlerThread(threadName);
+ handlerThread.start();
+
+ this.application = application;
+ this.handler = new Handler(handlerThread.getLooper());
+
+ scheduleIfNecessary();
+ }
+
+ /**
+ * Should be called whenever the underlying data of events has changed. Will appropriately
+ * schedule new event executions.
+ */
+ public void scheduleIfNecessary() {
+ handler.removeCallbacksAndMessages(null);
+
+ handler.post(() -> {
+ E event = getNextClosestEvent();
+
+ if (event != null) {
+ long delay = getDelayForEvent(event);
+
+ handler.postDelayed(() -> {
+ executeEvent(event);
+ scheduleIfNecessary();
+ }, delay);
+
+ scheduleAlarm(application, delay);
+ }
+ });
+ }
+
+ /**
+ * @return The next event that should be executed, or {@code null} if there are no events to execute.
+ */
+ @WorkerThread
+ protected @Nullable abstract E getNextClosestEvent();
+
+ /**
+ * Execute the provided event.
+ */
+ @WorkerThread
+ protected abstract void executeEvent(@NonNull E event);
+
+ /**
+ * @return How long before the provided event should be executed.
+ */
+ @WorkerThread
+ protected abstract long getDelayForEvent(@NonNull E event);
+
+ /**
+ * Schedules an alarm to call {@link #scheduleIfNecessary()} after the specified delay. You can
+ * use {@link #setAlarm(Context, long, Class)} as a helper method.
+ */
+ @AnyThread
+ protected abstract void scheduleAlarm(@NonNull Application application, long delay);
+
+ /**
+ * Helper method to set an alarm.
+ */
+ protected static void setAlarm(@NonNull Context context, long delay, @NonNull Class alarmClass) {
+ Intent intent = new Intent(context, alarmClass);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
+ AlarmManager alarmManager = ServiceUtil.getAlarmManager(context);
+
+ alarmManager.cancel(pendingIntent);
+ alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + delay, pendingIntent);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/sms/IncomingJoinedMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingJoinedMessage.java
index 8ce4bec8b4..658caddb1a 100644
--- a/src/org/thoughtcrime/securesms/sms/IncomingJoinedMessage.java
+++ b/src/org/thoughtcrime/securesms/sms/IncomingJoinedMessage.java
@@ -7,7 +7,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
public class IncomingJoinedMessage extends IncomingTextMessage {
public IncomingJoinedMessage(Address sender) {
- super(sender, 1, System.currentTimeMillis(), null, Optional.absent(), 0, false);
+ super(sender, 1, System.currentTimeMillis(), null, Optional.absent(), 0, 0, false);
}
@Override
diff --git a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java
index 92c53ea6d2..4ebddcd739 100644
--- a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java
+++ b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java
@@ -42,6 +42,7 @@ public class IncomingTextMessage implements Parcelable {
private final boolean push;
private final int subscriptionId;
private final long expiresInMillis;
+ private final long revealDuration;
private final boolean unidentified;
public IncomingTextMessage(@NonNull Context context, @NonNull SmsMessage message, int subscriptionId) {
@@ -55,6 +56,7 @@ public class IncomingTextMessage implements Parcelable {
this.sentTimestampMillis = message.getTimestampMillis();
this.subscriptionId = subscriptionId;
this.expiresInMillis = 0;
+ this.revealDuration = 0;
this.groupId = null;
this.push = false;
this.unidentified = false;
@@ -62,7 +64,7 @@ public class IncomingTextMessage implements Parcelable {
public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
String encodedBody, Optional group,
- long expiresInMillis, boolean unidentified)
+ long expiresInMillis, long revealDuration, boolean unidentified)
{
this.message = encodedBody;
this.sender = sender;
@@ -75,6 +77,7 @@ public class IncomingTextMessage implements Parcelable {
this.push = true;
this.subscriptionId = -1;
this.expiresInMillis = expiresInMillis;
+ this.revealDuration = revealDuration;
this.unidentified = unidentified;
if (group.isPresent()) {
@@ -97,6 +100,7 @@ public class IncomingTextMessage implements Parcelable {
this.push = (in.readInt() == 1);
this.subscriptionId = in.readInt();
this.expiresInMillis = in.readLong();
+ this.revealDuration = in.readLong();
this.unidentified = in.readInt() == 1;
}
@@ -113,6 +117,7 @@ public class IncomingTextMessage implements Parcelable {
this.push = base.isPush();
this.subscriptionId = base.getSubscriptionId();
this.expiresInMillis = base.getExpiresIn();
+ this.revealDuration = base.getRevealDuration();
this.unidentified = base.isUnidentified();
}
@@ -135,6 +140,7 @@ public class IncomingTextMessage implements Parcelable {
this.push = fragments.get(0).isPush();
this.subscriptionId = fragments.get(0).getSubscriptionId();
this.expiresInMillis = fragments.get(0).getExpiresIn();
+ this.revealDuration = fragments.get(0).getRevealDuration();
this.unidentified = fragments.get(0).isUnidentified();
}
@@ -152,6 +158,7 @@ public class IncomingTextMessage implements Parcelable {
this.push = true;
this.subscriptionId = -1;
this.expiresInMillis = 0;
+ this.revealDuration = 0;
this.unidentified = false;
}
@@ -163,6 +170,10 @@ public class IncomingTextMessage implements Parcelable {
return expiresInMillis;
}
+ public long getRevealDuration() {
+ return revealDuration;
+ }
+
public long getSentTimestampMillis() {
return sentTimestampMillis;
}
@@ -269,6 +280,8 @@ public class IncomingTextMessage implements Parcelable {
out.writeParcelable(groupId, flags);
out.writeInt(push ? 1 : 0);
out.writeInt(subscriptionId);
+ out.writeLong(expiresInMillis);
+ out.writeLong(revealDuration);
out.writeInt(unidentified ? 1 : 0);
}
}
diff --git a/src/org/thoughtcrime/securesms/util/GroupUtil.java b/src/org/thoughtcrime/securesms/util/GroupUtil.java
index 2ca0a710ab..71f8baf138 100644
--- a/src/org/thoughtcrime/securesms/util/GroupUtil.java
+++ b/src/org/thoughtcrime/securesms/util/GroupUtil.java
@@ -73,7 +73,7 @@ public class GroupUtil {
.setType(GroupContext.Type.QUIT)
.build();
- return Optional.of(new OutgoingGroupMediaMessage(groupRecipient, groupContext, null, System.currentTimeMillis(), 0, null, Collections.emptyList(), Collections.emptyList()));
+ return Optional.of(new OutgoingGroupMediaMessage(groupRecipient, groupContext, null, System.currentTimeMillis(), 0, 0, null, Collections.emptyList(), Collections.emptyList()));
}
diff --git a/src/org/thoughtcrime/securesms/util/IdentityUtil.java b/src/org/thoughtcrime/securesms/util/IdentityUtil.java
index d6d66ce9e5..6d00ed29ee 100644
--- a/src/org/thoughtcrime/securesms/util/IdentityUtil.java
+++ b/src/org/thoughtcrime/securesms/util/IdentityUtil.java
@@ -78,7 +78,7 @@ public class IdentityUtil {
SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId());
if (remote) {
- IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, false);
+ IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, 0, false);
if (verified) incoming = new IncomingIdentityVerifiedMessage(incoming);
else incoming = new IncomingIdentityDefaultMessage(incoming);
@@ -98,7 +98,7 @@ public class IdentityUtil {
}
if (remote) {
- IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.absent(), 0, false);
+ IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.absent(), 0, 0, false);
if (verified) incoming = new IncomingIdentityVerifiedMessage(incoming);
else incoming = new IncomingIdentityDefaultMessage(incoming);
@@ -128,14 +128,14 @@ public class IdentityUtil {
while ((groupRecord = reader.getNext()) != null) {
if (groupRecord.getMembers().contains(recipient.getAddress()) && groupRecord.isActive()) {
SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId());
- IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, false);
+ IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, 0, false);
IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming);
smsDatabase.insertMessageInbox(groupUpdate);
}
}
- IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.absent(), 0, false);
+ IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.absent(), 0, 0, false);
IncomingIdentityUpdateMessage individualUpdate = new IncomingIdentityUpdateMessage(incoming);
Optional insertResult = smsDatabase.insertMessageInbox(individualUpdate);
diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java
index f0f3017b3e..bcece8c8f4 100644
--- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java
+++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java
@@ -183,6 +183,8 @@ public class TextSecurePreferences {
private static final String MEDIA_KEYBOARD_MODE = "pref_media_keyboard_mode";
+ private static final String REVEALABLE_MESSAGE_DEFAULT = "pref_revealable_message_default";
+
public static boolean isScreenLockEnabled(@NonNull Context context) {
return getBooleanPreference(context, SCREEN_LOCK, false);
}
@@ -1098,6 +1100,14 @@ public class TextSecurePreferences {
return MediaKeyboardMode.valueOf(name);
}
+ public static void setIsRevealableMessageEnabled(Context context, boolean value) {
+ setBooleanPreference(context, REVEALABLE_MESSAGE_DEFAULT, value);
+ }
+
+ public static boolean isRevealableMessageEnabled(Context context) {
+ return getBooleanPreference(context, REVEALABLE_MESSAGE_DEFAULT, false);
+ }
+
public static void setBooleanPreference(Context context, String key, boolean value) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply();
}