diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java index 0056718e3c..66482c9c57 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java @@ -52,6 +52,8 @@ import androidx.viewpager.widget.ViewPager; import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager; import com.google.android.material.tabs.TabLayout; +import org.session.libsession.messaging.messages.control.DataExtractionNotification; +import org.session.libsession.messaging.sending_receiving.MessageSender; import org.session.libsession.messaging.threads.Address; import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import org.thoughtcrime.securesms.database.MediaDatabase; @@ -351,6 +353,12 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity { saveTask.executeOnExecutor(THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[attachments.size()])); actionMode.finish(); + // Sending a Data extraction notification (for incoming attachments only) + boolean containsIncoming = mediaRecords.parallelStream().anyMatch(m -> !m.isOutgoing()); + if (containsIncoming) { + //TODO uncomment line below when Data extraction will be activated + //sendMediaSavedNotificationIfNeeded(); + } } }.execute(); }) @@ -358,6 +366,16 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity { }, mediaRecords.size()); } + /** + * Send a MediaSaved notification to the recipient + */ + private void sendMediaSavedNotificationIfNeeded() { + // we don't send media saved notification for groups + if (recipient.isGroupRecipient()) return; + DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis())); + MessageSender.send(message, recipient.getAddress()); + } + @SuppressLint("StaticFieldLeak") private void handleDeleteMedia(@NonNull Collection mediaRecords) { int recordCount = mediaRecords.size(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 908943f5a8..6edfca5e9d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -39,7 +39,8 @@ import android.view.Window; import android.widget.FrameLayout; import android.widget.TextView; import android.widget.Toast; - +import org.session.libsession.messaging.messages.control.DataExtractionNotification; +import org.session.libsession.messaging.sending_receiving.MessageSender; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; @@ -52,7 +53,6 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; - import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.recipients.Recipient; @@ -352,11 +352,26 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im saveTask.executeOnExecutor( AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null)); + // Sending a Data extraction notification (for incoming attachments only) + if(!mediaItem.outgoing) { + //TODO uncomment line below when Data extraction will be activated + //sendMediaSavedNotificationIfNeeded(); + } }) .execute(); }); } + /** + * Send a MediaSaved notification to the recipient + */ + private void sendMediaSavedNotificationIfNeeded() { + // we don't send media saved notification for groups + if (conversationRecipient.isGroupRecipient()) return; + DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis())); + MessageSender.send(message, conversationRecipient.getAddress()); + } + @SuppressLint("StaticFieldLeak") private void deleteMedia() { MediaItem mediaItem = getCurrentMediaItem(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 0d30b8ce97..2aedad1119 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -57,6 +57,7 @@ import androidx.recyclerview.widget.RecyclerView.OnScrollListener; import com.annimon.stream.Stream; +import org.session.libsession.messaging.messages.control.DataExtractionNotification; import org.session.libsession.messaging.messages.visible.Quote; import org.session.libsession.messaging.messages.visible.VisibleMessage; import org.session.libsession.messaging.opengroups.OpenGroupAPI; @@ -745,6 +746,11 @@ public class ConversationFragment extends Fragment if (!Util.isEmpty(attachments)) { SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity()); saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0])); + // Sending a Data extraction notification (for incoming attachments only) + if(!message.isOutgoing()) { + //TODO uncomment line below when Data extraction will be activated + //sendMediaSavedNotificationIfNeeded(); + } return; } @@ -757,6 +763,16 @@ public class ConversationFragment extends Fragment }); } + /** + * Send a MediaSaved notification to the recipient + */ + private void sendMediaSavedNotificationIfNeeded() { + // we don't send media saved notification for groups + if (recipient.isGroupRecipient()) return; + DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis())); + MessageSender.send(message, recipient.getAddress()); + } + @Override public @NonNull Loader onCreateLoader(int id, Bundle args) { Log.i(TAG, "onCreateLoader"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 46e0bef279..91d1c4244b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -14,6 +14,7 @@ import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.session.libsession.messaging.sending_receiving.dataextraction.DataExtractionNotificationInfoMessage; import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt; @@ -104,6 +105,8 @@ public class ConversationUpdateItem extends LinearLayout else if (messageRecord.isCallLog()) setCallRecord(messageRecord); else if (messageRecord.isJoined()) setJoinedRecord(messageRecord); else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord); + else if (messageRecord.isScreenshotExtraction()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT); + else if (messageRecord.isMediaSavedExtraction()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED); else if (messageRecord.isEndSession()) setEndSessionRecord(messageRecord); else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord); else if (messageRecord.isIdentityVerified() || @@ -147,6 +150,22 @@ public class ConversationUpdateItem extends LinearLayout date.setVisibility(GONE); } + private void setDataExtractionRecord(final MessageRecord messageRecord, DataExtractionNotificationInfoMessage.Kind kind) { + @ColorInt int color = GeneralUtilitiesKt.getColorWithID(getResources(), R.color.text, getContext().getTheme()); + if (kind == DataExtractionNotificationInfoMessage.Kind.SCREENSHOT) { + icon.setImageResource(R.drawable.quick_camera_dark); + } else if (kind == DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED) { + icon.setImageResource(R.drawable.ic_file_download_white_36dp); + } + icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + + body.setText(messageRecord.getDisplayBody(getContext())); + + title.setVisibility(VISIBLE); + body.setVisibility(VISIBLE); + date.setVisibility(GONE); + } + private void setIdentityRecord(final MessageRecord messageRecord) { icon.setImageResource(R.drawable.ic_security_white_24dp); icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index d4a26b6ead..fd3c793d1f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -719,6 +719,14 @@ public class MmsDatabase extends MessagingDatabase { type |= Types.EXPIRATION_TIMER_UPDATE_BIT; } + if (retrieved.isScreenshotDataExtraction()) { + type |= Types.SCREENSHOT_EXTRACTION_BIT; + } + + if (retrieved.isMediaSavedDataExtraction()) { + type |= Types.MEDIA_SAVED_EXTRACTION_BIT; + } + return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index ae8a470069..024d1f51dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -72,6 +72,10 @@ public interface MmsSmsColumns { protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000; protected static final long GROUP_UPDATE_MESSAGE_BIT = 0x80000; + // Data Extraction Information + protected static final long MEDIA_SAVED_EXTRACTION_BIT = 0x01000; + protected static final long SCREENSHOT_EXTRACTION_BIT = 0x02000; + // Encrypted Storage Information XXX public static final long ENCRYPTION_MASK = 0xFF000000; // public static final long ENCRYPTION_SYMMETRIC_BIT = 0x80000000; Deprecated @@ -198,6 +202,14 @@ public interface MmsSmsColumns { return (type & EXPIRATION_TIMER_UPDATE_BIT) != 0; } + public static boolean isMediaSavedExtraction(long type) { + return (type & MEDIA_SAVED_EXTRACTION_BIT) != 0; + } + + public static boolean isScreenshotExtraction(long type) { + return (type & SCREENSHOT_EXTRACTION_BIT) != 0; + } + public static boolean isIncomingCall(long type) { return type == INCOMING_CALL_TYPE; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 7b2a0e293b..0a58ac42fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -16,9 +16,11 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.opengroups.OpenGroup import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.session.libsession.messaging.sending_receiving.dataextraction.DataExtractionNotificationInfoMessage import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.threads.Address.Companion.fromSerialized import org.session.libsession.messaging.threads.GroupRecord import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.messaging.utilities.UpdateMessageBuilder @@ -146,7 +148,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, val linkPreviews: Optional> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! }) val mmsDatabase = DatabaseFactory.getMmsDatabase(context) val insertResult = if (message.sender == getUserPublicKey()) { - val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointerAttachments, quote.orNull(), linkPreviews.orNull()?.firstOrNull()) mmsDatabase.beginTransaction() mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!) @@ -419,7 +420,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, true, null, listOf(), listOf()) val mmsDB = DatabaseFactory.getMmsDatabase(context) val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context) - if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return + if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) mmsDB.markAsSent(infoMessageID, true) } @@ -577,4 +578,26 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun getAttachmentThumbnailUri(attachmentId: AttachmentId): Uri { return PartAuthority.getAttachmentThumbnailUri(attachmentId) } + + // Data Extraction Notification + override fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) { + val database = DatabaseFactory.getMmsDatabase(context) + val address = fromSerialized(senderPublicKey) + val recipient = Recipient.from(context, address, false) + + if (recipient.isBlocked) return + + val mediaMessage = IncomingMediaMessage(address, sentTimestamp, -1, + 0, false, + false, + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.of(message)) + + database.insertSecureDecryptedMessageInbox(mediaMessage, -1) + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index d3be43d879..f98dcda5d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -131,6 +131,20 @@ public abstract class DisplayRecord { return SmsDatabase.Types.isExpirationTimerUpdate(type); } + // Data extraction + + public boolean isMediaSavedExtraction() { + return MmsSmsColumns.Types.isMediaSavedExtraction(type); + } + + public boolean isScreenshotExtraction() { + return MmsSmsColumns.Types.isScreenshotExtraction(type); + } + + public boolean isDataExtraction() { + return isMediaSavedExtraction() || isScreenshotExtraction(); + } + public boolean isCallLog() { return SmsDatabase.Types.isCallLog(type); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 631883eb5b..9ecb9bac2e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -25,6 +25,7 @@ import android.text.style.StyleSpan; import network.loki.messenger.R; +import org.session.libsession.messaging.sending_receiving.dataextraction.DataExtractionNotificationInfoMessage; import org.session.libsession.messaging.utilities.UpdateMessageBuilder; import org.session.libsession.messaging.utilities.UpdateMessageData; import org.thoughtcrime.securesms.database.MmsSmsColumns; @@ -97,6 +98,9 @@ public abstract class MessageRecord extends DisplayRecord { } else if (isExpirationTimerUpdate()) { int seconds = (int) (getExpiresIn() / 1000); return new SpannableString(UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, getIndividualRecipient().getAddress().serialize(), isOutgoing())); + } else if (isDataExtraction()) { + if (isScreenshotExtraction()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize()))); + else if (isMediaSavedExtraction()) return new SpannableString((UpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize()))); } // TODO below lines are left here for compatibility with older group update messages, it can be deleted later on else if (isGroupUpdate() && isOutgoing()) { @@ -159,7 +163,7 @@ public abstract class MessageRecord extends DisplayRecord { } public boolean isUpdate() { - return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() || + return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() || isDataExtraction() || isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || isLokiSessionRestoreSent() || isLokiSessionRestoreDone(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index eab5931c84..3315ba94f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -105,12 +105,16 @@ public class ThreadRecord extends DisplayRecord { } else if (SmsDatabase.Types.isJoinedType(type)) { return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, getRecipient().toShortString())); } else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) { - int seconds = (int)(getExpiresIn() / 1000); + int seconds = (int) (getExpiresIn() / 1000); if (seconds <= 0) { return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_messages_disabled)); } String time = ExpirationUtil.getExpirationDisplayValue(context, seconds); return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time)); + } else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) { + return emphasisAdded(context.getString(R.string.ThreadRecord_media_saved_by_s, getRecipient().toShortString())); + } else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) { + return emphasisAdded(context.getString(R.string.ThreadRecord_s_took_a_screenshot, getRecipient().toShortString())); } else if (SmsDatabase.Types.isIdentityUpdate(type)) { if (getRecipient().isGroupRecipient()) return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed)); else return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, getRecipient().toShortString())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java index dd23927bc3..05fed4b9d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java @@ -117,6 +117,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); //insert the timer update message database.insertSecureDecryptedMessageInbox(mediaMessage, -1); diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/DataExtractionNotificationManager.kt b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/DataExtractionNotificationManager.kt deleted file mode 100644 index 0c8fa779d4..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/DataExtractionNotificationManager.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.thoughtcrime.securesms.sskenvironment - -class DataExtractionNotificationManager { -} \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f156dad77b..5fbc37a757 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -440,6 +440,8 @@ %1$s a désactivé les messages éphémères. Vous avez défini l’expiration des messages éphémères à %1$s. %1$s a défini l’expiration des messages éphémères à %2$s. + %1$s a pris une capture d’écran. + %1$s a sauvegardé un media. Votre numéro de sécurité avec %s a changé. Vous avez marqué votre numéro de sécurité avec %s comme vérifié Vous avez marqué votre numéro de sécurité avec %s comme vérifié à partir d’un autre appareil diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7279e90ce..697b85ff20 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -515,6 +515,8 @@ %1$s disabled disappearing messages. You set the disappearing message timer to %1$s %1$s set the disappearing message timer to %2$s + %1$s took a screenshot. + Media saved by %1$s. Your safety number with %s has changed. You marked your safety number with %s verified You marked your safety number with %s verified from another device @@ -713,6 +715,8 @@ %s is on Session! Disappearing messages disabled Disappearing message time set to %s + %s took a screenshot. + Media saved by %s. Safety number changed Your safety number with %s has changed. You marked verified diff --git a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt index d7d5624e24..da84c8f6e8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -11,6 +11,7 @@ import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.opengroups.OpenGroup import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId +import org.session.libsession.messaging.sending_receiving.dataextraction.DataExtractionNotificationInfoMessage import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel @@ -159,4 +160,7 @@ interface StorageProtocol { // Message Handling /// Returns the ID of the `TSIncomingMessage` that was constructed. fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List, groupPublicKey: String?, openGroupID: String?, attachments: List): Long? -} \ No newline at end of file + + // Data Extraction Notification + fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt index 0526fbec0f..538b0b0001 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt @@ -9,7 +9,7 @@ class DataExtractionNotification(): ControlMessage() { // Kind enum sealed class Kind { class Screenshot() : Kind() - class MediaSaved(val timestanp: Long) : Kind() + class MediaSaved(val timestamp: Long) : Kind() val description: String = when(this) { @@ -46,7 +46,7 @@ class DataExtractionNotification(): ControlMessage() { val kind = kind ?: return false return when(kind) { is Kind.Screenshot -> true - is Kind.MediaSaved -> kind.timestanp > 0 + is Kind.MediaSaved -> kind.timestamp > 0 } } @@ -62,7 +62,7 @@ class DataExtractionNotification(): ControlMessage() { is Kind.Screenshot -> dataExtractionNotification.type = SignalServiceProtos.DataExtractionNotification.Type.SCREENSHOT is Kind.MediaSaved -> { dataExtractionNotification.type = SignalServiceProtos.DataExtractionNotification.Type.MEDIA_SAVED - dataExtractionNotification.timestamp = kind.timestanp + dataExtractionNotification.timestamp = kind.timestamp } } val contentProto = SignalServiceProtos.Content.newBuilder() @@ -73,5 +73,4 @@ class DataExtractionNotification(): ControlMessage() { return null } } - -} \ No newline at end of file +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java index 64b8dcf2b2..f235e84e92 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java @@ -3,10 +3,11 @@ package org.session.libsession.messaging.messages.signal; import org.session.libsession.messaging.messages.visible.VisibleMessage; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment; -import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; -import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; +import org.session.libsession.messaging.sending_receiving.dataextraction.DataExtractionNotificationInfoMessage; import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; import org.session.libsession.messaging.threads.Address; +import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; +import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; import org.session.libsession.utilities.GroupUtil; import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.service.api.messages.SignalServiceAttachment; @@ -26,9 +27,11 @@ public class IncomingMediaMessage { private final int subscriptionId; private final long expiresIn; private final boolean expirationUpdate; - private final QuoteModel quote; private final boolean unidentified; + private final DataExtractionNotificationInfoMessage dataExtractionNotification; + private final QuoteModel quote; + private final List attachments = new LinkedList<>(); private final List sharedContacts = new LinkedList<>(); private final List linkPreviews = new LinkedList<>(); @@ -44,17 +47,19 @@ public class IncomingMediaMessage { Optional> attachments, Optional quote, Optional> sharedContacts, - Optional> linkPreviews) + Optional> linkPreviews, + Optional dataExtractionNotification) { - this.push = true; - this.from = from; - this.sentTimeMillis = sentTimeMillis; - this.body = body.orNull(); - this.subscriptionId = subscriptionId; - this.expiresIn = expiresIn; - this.expirationUpdate = expirationUpdate; - this.quote = quote.orNull(); - this.unidentified = unidentified; + this.push = true; + this.from = from; + this.sentTimeMillis = sentTimeMillis; + this.body = body.orNull(); + this.subscriptionId = subscriptionId; + this.expiresIn = expiresIn; + this.expirationUpdate = expirationUpdate; + this.dataExtractionNotification = dataExtractionNotification.orNull(); + this.quote = quote.orNull(); + this.unidentified = unidentified; if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.INSTANCE.getEncodedId(group.get())); else this.groupId = null; @@ -73,7 +78,7 @@ public class IncomingMediaMessage { Optional> linkPreviews) { return new IncomingMediaMessage(from, message.getSentTimestamp(), -1, expiresIn, false, - false, Optional.fromNullable(message.getText()), group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews); + false, Optional.fromNullable(message.getText()), group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews, Optional.absent()); } public int getSubscriptionId() { @@ -116,6 +121,20 @@ public class IncomingMediaMessage { return groupId != null; } + public boolean isScreenshotDataExtraction() { + if (dataExtractionNotification == null) return false; + else { + return dataExtractionNotification.getKind() == DataExtractionNotificationInfoMessage.Kind.SCREENSHOT; + } + } + + public boolean isMediaSavedDataExtraction() { + if (dataExtractionNotification == null) return false; + else { + return dataExtractionNotification.getKind() == DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED; + } + } + public QuoteModel getQuote() { return quote; } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt index b860f02732..fc1b28e1cc 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt @@ -9,6 +9,7 @@ import org.session.libsession.messaging.messages.control.* import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment +import org.session.libsession.messaging.sending_receiving.dataextraction.DataExtractionNotificationInfoMessage import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel @@ -45,6 +46,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, is TypingIndicator -> handleTypingIndicator(message) is ClosedGroupControlMessage -> handleClosedGroupControlMessage(message) is ExpirationTimerUpdate -> handleExpirationTimerUpdate(message) + is DataExtractionNotification -> handleDataExtractionNotification(message) is ConfigurationMessage -> handleConfigurationMessage(message) is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID) } @@ -91,6 +93,24 @@ private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimer } } +// Data Extraction Notification handling + +private fun MessageReceiver.handleDataExtractionNotification(message: DataExtractionNotification) { + // we don't handle data extraction messages for groups (they shouldn't be sent, but in case we filter them here too) + if (message.groupPublicKey != null) return + + val storage = MessagingConfiguration.shared.storage + val senderPublicKey = message.sender!! + val notification: DataExtractionNotificationInfoMessage = when(message.kind) { + is DataExtractionNotification.Kind.Screenshot -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.SCREENSHOT) + is DataExtractionNotification.Kind.MediaSaved -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED) + else -> return + } + storage.insertDataExtractionNotificationMessage(senderPublicKey, notification, message.sentTimestamp!!) +} + +// Configuration message handling + private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMessage) { val context = MessagingConfiguration.shared.context val storage = MessagingConfiguration.shared.storage @@ -153,7 +173,8 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS } } // Get or create thread - val threadID = storage.getOrCreateThreadIdFor(message.syncTarget ?: message.sender!!, message.groupPublicKey, openGroupID) + val threadID = storage.getOrCreateThreadIdFor(message.syncTarget + ?: message.sender!!, message.groupPublicKey, openGroupID) // Parse quote if needed var quoteModel: QuoteModel? = null if (message.quote != null && proto.dataMessage.hasQuote()) { @@ -242,7 +263,7 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli storage.updateMembers(groupID, members.map { Address.fromSerialized(it) }) } else { storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), - null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp) + null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp) val userPublicKey = TextSecurePreferences.getLocalNumber(context) // Notify the user if (userPublicKey == sender) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/dataextraction/DataExtractionNotificationInfoMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/dataextraction/DataExtractionNotificationInfoMessage.kt new file mode 100644 index 0000000000..aca360eca6 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/dataextraction/DataExtractionNotificationInfoMessage.kt @@ -0,0 +1,16 @@ +package org.session.libsession.messaging.sending_receiving.dataextraction + +class DataExtractionNotificationInfoMessage { + + enum class Kind { + SCREENSHOT, + MEDIA_SAVED + } + + var kind: Kind? = null + + constructor(kind: Kind?) { + this.kind = kind + } + +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt index 3999e13e6b..4239c8bdb2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt @@ -3,6 +3,7 @@ package org.session.libsession.messaging.utilities import android.content.Context import org.session.libsession.R import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.messaging.sending_receiving.dataextraction.DataExtractionNotificationInfoMessage import org.session.libsession.utilities.ExpirationUtil import org.session.libsignal.service.api.messages.SignalServiceGroup @@ -78,7 +79,7 @@ object UpdateMessageBuilder { if (!isOutgoing && sender == null) return "" val senderName: String? = if (!isOutgoing) { MessagingConfiguration.shared.storage.getDisplayNameForRecipient(sender!!) ?: sender - } else { sender } + } else { context.getString(R.string.MessageRecord_you) } return if (duration <= 0) { if (isOutgoing) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages) else context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, senderName) @@ -89,8 +90,13 @@ object UpdateMessageBuilder { } } - //TODO do this when the current update is merged - fun buildDataExtractionMessage(): String { - return "" + fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, sender: String? = null): String { + val senderName = MessagingConfiguration.shared.storage.getDisplayNameForRecipient(sender!!) ?: sender + return when (kind) { + DataExtractionNotificationInfoMessage.Kind.SCREENSHOT -> + context.getString(R.string.MessageRecord_s_took_a_screenshot, senderName) + DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED -> + context.getString(R.string.MessageRecord_media_saved_by_s, senderName) + } } } diff --git a/libsession/src/main/res/values/strings.xml b/libsession/src/main/res/values/strings.xml index 116b106017..ee0510385c 100644 --- a/libsession/src/main/res/values/strings.xml +++ b/libsession/src/main/res/values/strings.xml @@ -509,6 +509,8 @@ %1$s disabled disappearing messages. You set the disappearing message timer to %1$s %1$s set the disappearing message timer to %2$s + %1$s took a screenshot. + Media saved by %1$s. Your safety number with %s has changed. You marked your safety number with %s verified You marked your safety number with %s verified from another device