From 67d4f666ce74e8560384d11796904fa40d1cb448 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Tue, 18 Jul 2023 13:58:13 -0400 Subject: [PATCH] Add share highwater timestamp protection to CFv2. --- .../conversation/drafts/DraftRepository.kt | 16 +++++++++++++--- .../conversation/drafts/DraftViewModel.kt | 4 ++-- .../conversation/v2/ConversationActivity.kt | 8 ++++---- .../conversation/v2/ConversationFragment.kt | 6 +++++- .../v2/ShareDataTimestampViewModel.kt | 16 ++++++++++++++++ 5 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ShareDataTimestampViewModel.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt index d64ba8b5bb..363b8c2576 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt @@ -62,8 +62,8 @@ class DraftRepository( val TAG = Log.tag(DraftRepository::class.java) } - fun getShareOrDraftData(): Maybe> { - return MaybeCompat.fromCallable { getShareOrDraftDataInternal() } + fun getShareOrDraftData(lastShareDataTimestamp: Long): Maybe> { + return MaybeCompat.fromCallable { getShareOrDraftDataInternal(lastShareDataTimestamp) } .observeOn(Schedulers.io()) } @@ -73,7 +73,17 @@ class DraftRepository( * * Note: Voice note drafts are handled differently and via the [DraftViewModel.state] */ - private fun getShareOrDraftDataInternal(): Pair? { + @Suppress("ConvertTwoComparisonsToRangeCheck") + private fun getShareOrDraftDataInternal(lastShareDataTimestamp: Long): Pair? { + val sharedDataTimestamp: Long = conversationArguments?.shareDataTimestamp ?: -1 + Log.d(TAG, "Shared this data at $sharedDataTimestamp and last processed share data at $lastShareDataTimestamp") + if (sharedDataTimestamp > 0 && sharedDataTimestamp <= lastShareDataTimestamp) { + Log.d(TAG, "Already processed this share data. Skipping.") + return null + } else { + Log.d(TAG, "Have not processed this share data. Proceeding.") + } + val shareText = conversationArguments?.draftText val shareMedia = conversationArguments?.draftMedia val shareContentType = conversationArguments?.draftContentType diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt index c965e0ab35..f171c08220 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt @@ -168,8 +168,8 @@ class DraftViewModel @JvmOverloads constructor( .observeOn(AndroidSchedulers.mainThread()) } - fun loadShareOrDraftData(): Maybe { - return repository.getShareOrDraftData() + fun loadShareOrDraftData(lastShareDataTimestamp: Long): Maybe { + return repository.getShareOrDraftData(lastShareDataTimestamp) .doOnSuccess { (_, drafts) -> if (drafts != null) { store.update { saveDrafts(it.copyAndSetDrafts(drafts = drafts)) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt index 4da206c16d..61b8cc25e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt @@ -28,7 +28,6 @@ class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControl private val theme = DynamicNoActionBarTheme() private val transitionDebouncer: Debouncer = Debouncer(150, TimeUnit.MILLISECONDS) - private var shareDataTimestamp: Long = -1L override val voiceNoteMediaController = VoiceNoteMediaController(this, true) @@ -36,6 +35,7 @@ class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControl override val googlePayResultPublisher: Subject = PublishSubject.create() private val motionEventRelay: MotionEventRelay by viewModels() + private val shareDataTimestampViewModel: ShareDataTimestampViewModel by viewModels() override fun onPreCreate() { theme.onCreate(this) @@ -47,9 +47,9 @@ class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControl window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) if (savedInstanceState != null) { - shareDataTimestamp = savedInstanceState.getLong(STATE_WATERMARK, -1L) + shareDataTimestampViewModel.timestamp = savedInstanceState.getLong(STATE_WATERMARK, -1L) } else if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) { - shareDataTimestamp = System.currentTimeMillis() + shareDataTimestampViewModel.timestamp = System.currentTimeMillis() } setContentView(R.layout.fragment_container) @@ -66,7 +66,7 @@ class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControl override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putLong(STATE_WATERMARK, shareDataTimestamp) + outState.putLong(STATE_WATERMARK, shareDataTimestampViewModel.timestamp) } override fun onDestroy() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 5bd8aa6b60..6a388ef2a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -407,6 +407,8 @@ class ConversationFragment : InlineQueryViewModelV2(recipientRepository = conversationRecipientRepository) } + private val shareDataTimestampViewModel: ShareDataTimestampViewModel by activityViewModels() + private val inlineQueryController: InlineQueryResultsControllerV2 by lazy { InlineQueryResultsControllerV2( this, @@ -907,7 +909,7 @@ class ConversationFragment : this::handleReplyToMessage ).attachToRecyclerView(binding.conversationItemRecycler) - draftViewModel.loadShareOrDraftData() + draftViewModel.loadShareOrDraftData(shareDataTimestampViewModel.timestamp) .subscribeBy { data -> handleShareOrDraftData(data) } .addTo(disposables) @@ -1298,6 +1300,8 @@ class ConversationFragment : } private fun handleShareOrDraftData(data: ShareOrDraftData) { + shareDataTimestampViewModel.timestamp = args.shareDataTimestamp + when (data) { is ShareOrDraftData.SendKeyboardImage -> sendMessageWithoutComposeInput(slide = data.slide, clearCompose = false) is ShareOrDraftData.SendSticker -> sendMessageWithoutComposeInput(slide = data.slide, clearCompose = true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ShareDataTimestampViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ShareDataTimestampViewModel.kt new file mode 100644 index 0000000000..5131785484 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ShareDataTimestampViewModel.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation.v2 + +import androidx.lifecycle.ViewModel + +/** + * Hold the last share timestamp in an activity scoped view model for sharing between + * the activity and fragments. + */ +class ShareDataTimestampViewModel : ViewModel() { + var timestamp: Long = -1L +}