Add support for view-once messages.

This commit is contained in:
Greyson Parrelli 2019-06-11 02:18:45 -04:00
parent 9f7bb69341
commit c77809fa90
80 changed files with 1803 additions and 148 deletions

View File

@ -299,7 +299,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediasend.MediaSendActivity"
android:theme="@style/TextSecure.DarkNoActionBar"
android:theme="@style/TextSecure.FullScreenMedia"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@ -336,6 +336,13 @@
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".revealable.RevealableMessageActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.FullScreenMedia"
android:windowSoftInputMode="stateHidden"
android:excludeFromRecents="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".stickers.StickerManagementActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightTheme"
@ -577,6 +584,8 @@
<receiver android:name=".service.ExpirationListener" />
<receiver android:name=".revealable.RevealableMessageManager$RevealAlarm" />
<provider android:name=".providers.PartProvider"
android:grantUriPermissions="true"
android:exported="false"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,1C5.9,1 1,5.9 1,12s4.9,11 11,11s11,-4.9 11,-11S18.1,1 12,1zM12,21.5c-5.2,0 -9.5,-4.3 -9.5,-9.5S6.8,2.5 12,2.5s9.5,4.3 9.5,9.5C21.5,17.2 17.2,21.5 12,21.5zM17,12.2l1.1,1.1l-5.5,5.5c-0.3,0.3 -0.8,0.3 -1.1,0l-5.5,-5.5L7,12.2l3.6,3.6c0,0 0.3,0.4 0.7,1V5h1.5v11.8c0.4,-0.6 0.7,-1 0.7,-1L17,12.2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M5.5,3.6 L20.006,12 5.5,20.4V3.6M4.72,1.575c-0.426,0 -0.72,0.339 -0.72,0.925v19c0,0.586 0.294,0.925 0.72,0.925a1.168,1.168 0,0 0,0.578 -0.177l16.4,-9.5a0.8,0.8 0,0 0,0 -1.5L5.3,1.752a1.168,1.168 0,0 0,-0.578 -0.177Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M21.7,11.248a0.8,0.8 0,0 1,0 1.5l-16.4,9.5c-0.714,0.414 -1.3,0.077 -1.3,-0.748V2.5c0,-0.825 0.584,-1.162 1.3,-0.748Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M13,14.5c0,0.6 -0.4,1 -1,1s-1,-0.4 -1,-1L11.5,8h1L13,14.5zM21,14c0,5 -4,9 -9,9s-9,-4 -9,-9c0,-2 0.7,-4 1.9,-5.6C4.3,8.3 3.9,7.6 4,6.9S4.9,5.9 5.5,6C6,6.2 6.3,6.5 6.4,6.9c1.4,-1.1 3,-1.8 4.7,-1.9L10,1h4l-1.2,4c1.7,0.2 3.4,0.8 4.8,1.9c0.2,-0.7 0.8,-1.1 1.5,-0.9c0.7,0.2 1.1,0.8 0.9,1.5c-0.1,0.4 -0.5,0.8 -0.9,0.9C20.3,10 21,12 21,14zM19.5,14c0,-4.1 -3.4,-7.5 -7.5,-7.5S4.5,9.9 4.5,14s3.4,7.5 7.5,7.5S19.5,18.1 19.5,14z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M7.9,20.3l-1.1,1.1c4.1,2.8 9.7,1.8 12.5,-2.3c2.1,-3.1 2.1,-7.2 0,-10.3l-1.1,1.1c2.3,3.5 1.3,8.1 -2.2,10.4C13.6,21.9 10.4,21.9 7.9,20.3zM3.5,22.5L21,5l-1,-1l-2.8,2.7c-1.3,-1 -2.8,-1.5 -4.4,-1.7L14,1h-4l1.2,4C9.5,5.2 7.8,5.8 6.4,6.9C6.3,6.3 5.6,5.9 4.9,6S3.9,6.9 4,7.5C4.2,8 4.5,8.3 4.9,8.4C3.7,10 3,12 3,14c0,1.9 0.6,3.7 1.7,5.2l-2.2,2.2L3.5,22.5zM5.8,18.2c-2.3,-3.4 -1.4,-8.1 2.1,-10.4c2.5,-1.7 5.8,-1.7 8.3,0l-3.4,3.4L12.5,8h-1l-0.4,4.8L5.8,18.2z"/>
</vector>

View File

@ -153,6 +153,16 @@
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding" />
<ViewStub
android:id="@+id/revealable_view_stub"
android:layout="@layout/conversation_item_received_revealable"
android:layout_width="148dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/conversation_item_body"
android:layout_width="wrap_content"

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.revealable.RevealableMessageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/revealable_view"
android:layout_width="148dp"
android:layout_height="wrap_content"
android:visibility="gone"
app:revealable_unopenedForegroundColor="?conversation_item_received_text_primary_color"
app:revealable_openedForegroundColor="?conversation_item_sent_text_primary_color"
tools:visibility="visible"/>

View File

@ -94,6 +94,16 @@
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding" />
<ViewStub
android:id="@+id/revealable_view_stub"
android:layout="@layout/conversation_item_sent_revealable"
android:layout_width="148dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_top_padding"
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/conversation_item_body"
android:layout_width="wrap_content"

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.revealable.RevealableMessageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/revealable_view"
android:layout_width="148dp"
android:layout_height="wrap_content"
android:visibility="gone"
app:revealable_unopenedForegroundColor="?conversation_item_sent_text_primary_color"
app:revealable_openedForegroundColor="?conversation_item_sent_text_primary_color"
tools:visibility="visible"/>

View File

@ -61,6 +61,15 @@
android:layout_marginBottom="12dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/mediasend_reveal_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:layout_marginBottom="4dp"
android:layout_gravity="bottom"
tools:src="@drawable/ic_view_infinite_32" />
<LinearLayout
android:id="@+id/mediasend_compose_container"
android:layout_width="0dp"

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/core_black">
<ImageView
android:id="@+id/reveal_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"/>
<ImageView
android:id="@+id/reveal_close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="19dp"
android:layout_marginStart="19dp"
android:tint="@color/core_white"
app:srcCompat="@drawable/ic_x"/>
</FrameLayout>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:parentTag="android.widget.LinearLayout">
<FrameLayout
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:layout_gravity="center_vertical">
<ImageView
android:id="@+id/revealable_icon"
android:layout_width="24dp"
android:layout_height="24dp"
tools:src="@drawable/ic_play_solid_24"/>
<com.pnikosis.materialishprogress.ProgressWheel
android:id="@+id/revealable_progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
app:matProg_barColor="@color/core_white"
app:matProg_rimColor="@color/transparent"
app:matProg_linearProgress="true"
app:matProg_spinSpeed="0.2"
app:matProg_barWidth="2dp"
app:matProg_rimWidth="2dp"
app:matProg_circleRadius="24dp"
tools:visibility="visible"/>
</FrameLayout>
<TextView
android:id="@+id/revealable_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
style="@style/Signal.Text.Preview"
android:fontFamily="sans-serif-medium"
android:maxLines="2"
android:ellipsize="end"
tools:text="@string/RevealableMessageView_view_photo" />
</merge>

View File

@ -94,6 +94,7 @@
<attr name="conversation_item_sticky_date_background" format="reference" />
<attr name="conversation_item_sticky_date_text_color" format="color" />
<attr name="conversation_item_image_outline_color" format="color" />
<attr name="conversation_item_reveal_viewed_background_color" format="color" />
<attr name="dialog_info_icon" format="reference" />
<attr name="dialog_alert_icon" format="reference" />
@ -347,4 +348,9 @@
<attr name="labeledEditText_background" format="color" />
<attr name="labeledEditText_textLayout" format="reference" />
</declare-styleable>
<declare-styleable name="RevealableMessageView">
<attr name="revealable_unopenedForegroundColor" format="color" />
<attr name="revealable_openedForegroundColor" format="color" />
</declare-styleable>
</resources>

View File

@ -639,6 +639,11 @@
<string name="RegistrationActivity_enter_the_code_we_sent_to_s">Enter the code we sent to %s</string>
<string name="RegistrationActivity_call">Call</string>
<!-- RevealableMessageView -->
<string name="RevealableMessageView_view_photo">View Photo</string>
<string name="RevealableMessageView_viewed">Viewed</string>
<string name="RevealableMessageView_photo">Photo</string>
<!-- ScribbleActivity -->
<string name="ScribbleActivity_save_failure">Failed to save image changes</string>
@ -677,6 +682,7 @@
<string name="SmsMessageRecord_secure_session_reset_s">%s reset the secure session.</string>
<string name="SmsMessageRecord_duplicate_message">Duplicate message.</string>
<string name="SmsMessageRecord_this_message_could_not_be_processed_because_it_was_sent_from_a_newer_version">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.</string>
<string name="SmsMessageRecord_error_handling_incoming_message">Error handling incoming message</string>
<!-- StickerManagementActivity -->
<string name="StickerManagementActivity_stickers">Stickers</string>
@ -707,6 +713,8 @@
<string name="ThreadRecord_called_you">Called you</string>
<string name="ThreadRecord_missed_call">Missed call</string>
<string name="ThreadRecord_media_message">Media message</string>
<string name="ThreadRecord_sticker">Sticker</string>
<string name="ThreadRecord_disappearing_photo">Disappearing photo</string>
<string name="ThreadRecord_s_is_on_signal">%s is on Signal!</string>
<string name="ThreadRecord_disappearing_messages_disabled">Disappearing messages disabled</string>
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Disappearing message time set to %s</string>
@ -794,6 +802,7 @@
<string name="MessageNotifier_mark_read">Mark read</string>
<string name="MessageNotifier_media_message">Media message</string>
<string name="MessageNotifier_sticker">Sticker</string>
<string name="MessageNotifier_disappearing_photo">Disappearing photo</string>
<string name="MessageNotifier_reply">Reply</string>
<string name="MessageNotifier_signal_message">Signal Message</string>
<string name="MessageNotifier_unsecured_sms">Unsecured SMS</string>

View File

@ -219,6 +219,7 @@
<item name="conversation_item_sticky_date_background">@drawable/sticky_date_header_background_light</item>
<item name="conversation_item_sticky_date_text_color">@color/core_grey_60</item>
<item name="conversation_item_image_outline_color">@color/transparent_black_30</item>
<item name="conversation_item_reveal_viewed_background_color">@color/core_white</item>
<item name="quick_camera_icon">@drawable/quick_camera_light</item>
<item name="quick_mic_icon">@drawable/ic_mic_grey600_24dp</item>
@ -355,6 +356,7 @@
<item name="conversation_item_sticky_date_background">@drawable/sticky_date_header_background_dark</item>
<item name="conversation_item_sticky_date_text_color">@color/core_grey_25</item>
<item name="conversation_item_image_outline_color">@color/transparent_white_30</item>
<item name="conversation_item_reveal_viewed_background_color">@color/core_black</item>
<item name="contact_list_divider">@drawable/contact_list_divider_dark</item>
@ -495,4 +497,8 @@
<style name="TextSecure.DarkRegistrationTheme" parent="TextSecure.DarkNoActionBar">
</style>
<style name="TextSecure.FullScreenMedia" parent="TextSecure.DarkNoActionBar">
<item name="android:windowFullscreen">true</item>
</style>
</resources>

View File

@ -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;
@ -91,6 +92,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
private static final String TAG = ApplicationContext.class.getSimpleName();
private ExpiringMessageManager expiringMessageManager;
private RevealableMessageManager revealableMessageManager;
private TypingStatusRepository typingStatusRepository;
private TypingStatusSender typingStatusSender;
private JobManager jobManager;
@ -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();
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Void>() {
@ -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<Void> sendMediaMessage(final boolean forceSms,
@ -2159,6 +2164,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
List<Contact> contacts,
List<LinkPreview> 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<Void> 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<Void>() {
sendMediaMessage(forceSms, "", slideDeck, inputPanel.getQuote().orNull(), Collections.emptyList(), Collections.emptyList(), expiresIn, 0, subscriptionId, initiating, true).addListener(new AssertedSuccessListener<Void>() {
@Override
public void onSuccess(Void nothing) {
new AsyncTask<Void, Void, Void>() {
@ -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);
}
}

View File

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

View File

@ -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<MessageRecord> batchSelected = new HashSet<>();
private @NonNull Outliner outliner = new Outliner();
private Recipient conversationRecipient;
private Stub<ConversationItemThumbnail> mediaThumbnailStub;
private Stub<AudioView> audioViewStub;
@ -158,6 +163,7 @@ public class ConversationItem extends LinearLayout
private Stub<SharedContactView> sharedContactStub;
private Stub<LinkPreviewView> linkPreviewStub;
private Stub<StickerView> stickerStub;
private Stub<RevealableMessageView> 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<Slide> 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<MessageRecord> previous, @NonNull Optional<MessageRecord> 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;
} else if (isStartOfMessageCluster(current, previous, isGroupThread)) {
background = current.isOutgoing() ? R.drawable.message_bubble_background_sent_start
: R.drawable.message_bubble_background_received_start;
} 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_alone;
outliner.setRadius(bigRadius);
} else {
background = current.isOutgoing() ? R.drawable.message_bubble_background_sent_middle
: R.drawable.message_bubble_background_received_middle;
background = R.drawable.message_bubble_background_received_alone;
outliner.setRadius(bigRadius);
}
} else if (isStartOfMessageCluster(current, previous, isGroupThread)) {
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)) {
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 {
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()) {

View File

@ -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<DatabaseAttachment> getPendingAttachments() {
final SQLiteDatabase database = databaseHelper.getReadableDatabase();
final List<DatabaseAttachment> 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();

View File

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

View File

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

View File

@ -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<RevealExpirationInfo> markRevealStarted(@NonNull SyncMessageId messageId, long proposedStartTime) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
List<RevealExpirationInfo> 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<Attachment> 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<NetworkFailure>(),
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);
}

View File

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

View File

@ -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<String> smsColumnsPresent = new HashSet<>();
smsColumnsPresent.add(MmsSmsColumns.ID);

View File

@ -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<IdentityKeyMismatch> getMismatches(String document) {

View File

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

View File

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

View File

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

View File

@ -54,14 +54,15 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
int partCount, long mailbox,
List<IdentityKeyMismatch> mismatches,
List<NetworkFailure> failures, int subscriptionId,
long expiresIn, long expireStarted, int readReceiptCount,
long expiresIn, long expireStarted,
long revealDuration, long revealStartTime, int readReceiptCount,
@Nullable Quote quote, @NonNull List<Contact> contacts,
@NonNull List<LinkPreview> 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;
}

View File

@ -22,12 +22,16 @@ public abstract class MmsMessageRecord extends MessageRecord {
private final @NonNull List<Contact> contacts = new LinkedList<>();
private final @NonNull List<LinkPreview> 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<IdentityKeyMismatch> mismatches,
List<NetworkFailure> 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<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews, boolean unidentified)
{
@ -35,6 +39,8 @@ public abstract class MmsMessageRecord extends MessageRecord {
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<LinkPreview> getLinkPreviews() {
return linkPreviews;
}
public long getRevealDuration() {
return revealDuration;
}
public long getRevealStartTime() {
return revealStartTime;
}
}

View File

@ -57,7 +57,7 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
super(id, "", conversationRecipient, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
new LinkedList<IdentityKeyMismatch>(), new LinkedList<NetworkFailure>(), 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;

View File

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

View File

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

View File

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

View File

@ -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> insertResult = smsDatabase.insertMessageInbox(groupMessage);

View File

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

View File

@ -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> insertResult = database.insertMessageInbox(message, contentLocation, threadId);
if (insertResult.isPresent()) {

View File

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

View File

@ -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<Long> 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<Attachment> sticker = getStickerAttachment(message.getMessage().getSticker());
Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts());
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getMessage().getPreviews(), message.getMessage().getBody().or(""));
List<Attachment> syncAttachments = PointerAttachment.forPointers(message.getMessage().getAttachments());
long messageTimer = message.getMessage().getMessageTimerInSeconds() * 1000;
List<Attachment> 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<SignalServiceGroup> group,
long timestamp,
@NonNull Optional<Long> smsMessageId)
{
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
if (!smsMessageId.isPresent()) {
Optional<InsertResult> 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<Long> 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<QuoteModel> getValidatedQuote(Optional<SignalServiceDataMessage.Quote> quote) {
if (!quote.isPresent()) return Optional.absent();
@ -1172,19 +1228,26 @@ public class PushDecryptJob extends BaseJob {
if (message.isMms()) {
MmsMessageRecord mmsMessage = (MmsMessageRecord) message;
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));
}
}
return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), false, attachments));
}
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);

View File

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

View File

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

View File

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

View File

@ -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,7 +66,7 @@ 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) {
@ -83,7 +85,7 @@ class MediaSendViewModel extends ViewModel {
this.body = "";
this.buttonState = ButtonState.GONE;
this.railState = RailState.GONE;
this.timerState = TimerState.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<Media> 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<Media> 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<Media> 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() {
// TODO[reveal]
RevealState updatedRevealState = RevealState.GONE;
List<Media> 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()));
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> 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;
}
}

View File

@ -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<String> body,
Optional<SignalServiceGroup> 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;
}

View File

@ -11,7 +11,7 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) {
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList(),
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, 0, null, Collections.emptyList(),
Collections.emptyList());
}

View File

@ -24,13 +24,14 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
@NonNull List<Attachment> avatar,
long sentTimeMillis,
long expiresIn,
long revealDuration,
@Nullable QuoteModel quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> 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<Contact> contacts,
@NonNull List<LinkPreview> previews)
@ -47,7 +49,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
super(recipient, Base64.encodeBytes(group.toByteArray()),
new LinkedList<Attachment>() {{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;
}

View File

@ -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<NetworkFailure> networkFailures = new LinkedList<>();
@ -32,7 +33,7 @@ public class OutgoingMediaMessage {
public OutgoingMediaMessage(Recipient recipient, String message,
List<Attachment> attachments, long sentTimeMillis,
int subscriptionId, long expiresIn,
int subscriptionId, long expiresIn, long revealDuration,
int distributionType,
@Nullable QuoteModel outgoingQuote,
@NonNull List<Contact> 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<Contact> contacts,
@NonNull List<LinkPreview> 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;
}

View File

@ -18,11 +18,12 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
long sentTimeMillis,
int distributionType,
long expiresIn,
long revealDuration,
@Nullable QuoteModel quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> 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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Optional<MmsMessageRecord>> 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<T> {
void onComplete(T result);
}
}

View File

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

View File

@ -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<Optional<MmsMessageRecord>> 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<Optional<MmsMessageRecord>> getMessage() {
return message;
}
@Override
protected void onCleared() {
application.getContentResolver().unregisterContentObserver(observer);
}
private void onMessageRetrieved(@NonNull Optional<MmsMessageRecord> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new RevealableMessageViewModel(application, messageId, repository));
}
}
}

View File

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

View File

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

View File

@ -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.<SignalServiceGroup>absent(), 0, false);
super(sender, 1, System.currentTimeMillis(), null, Optional.<SignalServiceGroup>absent(), 0, 0, false);
}
@Override

View File

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

View File

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

View File

@ -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> insertResult = smsDatabase.insertMessageInbox(individualUpdate);

View File

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