diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/received/ViewReceivedGiftViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/received/ViewReceivedGiftViewModel.kt index 46cf5683d5..1a1ad08d92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/received/ViewReceivedGiftViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/received/ViewReceivedGiftViewModel.kt @@ -70,6 +70,7 @@ class ViewReceivedGiftViewModel( override fun onCleared() { disposables.dispose() + store.dispose() } fun setChecked(isChecked: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/sent/ViewSentGiftViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/sent/ViewSentGiftViewModel.kt index 0c82cd7f0d..a9bedda9c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/sent/ViewSentGiftViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/sent/ViewSentGiftViewModel.kt @@ -38,6 +38,7 @@ class ViewSentGiftViewModel( override fun onCleared() { disposables.dispose() + store.dispose() } class Factory( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/donor/DonorErrorConfigurationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/donor/DonorErrorConfigurationViewModel.kt index bd3205f6ed..aee2f131bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/donor/DonorErrorConfigurationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/donor/DonorErrorConfigurationViewModel.kt @@ -62,6 +62,7 @@ class DonorErrorConfigurationViewModel : ViewModel() { override fun onCleared() { disposables.clear() + store.dispose() } fun setSelectedBadge(badgeIndex: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChipViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChipViewModel.kt index 0cd7ce6d0b..33e9b20b5b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChipViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChipViewModel.kt @@ -35,6 +35,7 @@ class ContactChipViewModel : ViewModel() { disposables.clear() disposableMap.values.forEach { it.dispose() } disposableMap.clear() + store.dispose() } fun add(selectedContact: SelectedContact) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java index a7d24b05e5..17e15105c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java @@ -59,6 +59,7 @@ import io.reactivex.rxjava3.core.BackpressureStrategy; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.processors.PublishProcessor; import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.subjects.BehaviorSubject; @@ -135,10 +136,12 @@ public class ConversationViewModel extends ViewModel { .map(Recipient::resolved) .subscribe(recipientCache); - conversationStateStore.update(Observable.combineLatest(recipientId.distinctUntilChanged(), conversationStateTick, (id, tick) -> id) - .switchMap(conversationRepository::getSecurityInfo) - .toFlowable(BackpressureStrategy.LATEST), - (securityInfo, state) -> state.withSecurityInfo(securityInfo)); + Disposable disposable = conversationStateStore.update(Observable.combineLatest(recipientId.distinctUntilChanged(), conversationStateTick, (id, tick) -> id) + .switchMap(conversationRepository::getSecurityInfo) + .toFlowable(BackpressureStrategy.LATEST), + (securityInfo, state) -> state.withSecurityInfo(securityInfo)); + + disposables.add(disposable); BehaviorSubject conversationMetadata = BehaviorSubject.create(); @@ -435,6 +438,7 @@ public class ConversationViewModel extends ViewModel { ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver); ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver); disposables.clear(); + conversationStateStore.dispose(); EventBus.getDefault().unregister(this); } 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 41ba3ee6f1..a97b9e7cb0 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 @@ -33,6 +33,10 @@ class DraftViewModel @JvmOverloads constructor( val voiceNoteDraft: Draft? get() = store.state.voiceNoteDraft + override fun onCleared() { + store.dispose() + } + fun setThreadId(threadId: Long) { store.update { it.copy(threadId = threadId) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt index 247d6cf0fa..d5cbe58280 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt @@ -41,5 +41,6 @@ class MediaPreviewV2ViewModel : ViewModel() { override fun onCleared() { disposables.dispose() + store.dispose() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureViewModel.kt index 40f201d544..564cf727a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureViewModel.kt @@ -26,6 +26,10 @@ class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : Vi } } + override fun onCleared() { + store.dispose() + } + fun onImageCaptured(data: ByteArray, width: Int, height: Int) { repository.renderImageToMedia(data, width, height, this::onMediaRendered, this::onMediaRenderFailed) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/pnp/WhoCanSeeMyPhoneNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/pnp/WhoCanSeeMyPhoneNumberViewModel.kt index a38c53ba02..0c8735cd49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/pnp/WhoCanSeeMyPhoneNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/pnp/WhoCanSeeMyPhoneNumberViewModel.kt @@ -29,5 +29,6 @@ class WhoCanSeeMyPhoneNumberViewModel : ViewModel() { override fun onCleared() { disposables.clear() + store.dispose() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java index ef3b6afec4..68a50a55b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java @@ -65,6 +65,7 @@ class UsernameEditViewModel extends ViewModel { protected void onCleared() { super.onCleared(); disposables.clear(); + uiState.dispose(); } void onNicknameUpdated(@NonNull String nickname) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/safety/SafetyNumberBottomSheetViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/safety/SafetyNumberBottomSheetViewModel.kt index 5cbd6847fa..2066277fa9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/safety/SafetyNumberBottomSheetViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/safety/SafetyNumberBottomSheetViewModel.kt @@ -66,6 +66,8 @@ class SafetyNumberBottomSheetViewModel( override fun onCleared() { disposables.clear() + destinationStore.dispose() + store.dispose() } fun getIdentityRecord(recipientId: RecipientId): Maybe { diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareViewModel.kt index acb5727b7d..95ad1a920b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareViewModel.kt @@ -72,6 +72,7 @@ class ShareViewModel( override fun onCleared() { disposables.clear() + store.dispose() } private fun moveToFailedState(throwable: Throwable? = null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt index 64ee05a75f..bb0d88f637 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt @@ -31,6 +31,7 @@ class ChooseInitialMyStoryMembershipViewModel @JvmOverloads constructor( override fun onCleared() { disposables.clear() + store.dispose() } fun select(selection: DistributionListPrivacyMode): Single { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt index a17d08ed08..f326015845 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt @@ -59,13 +59,14 @@ class StoriesPrivacySettingsViewModel : ViewModel() { pagingController.set(observablePagedData.controller) - store.update(observablePagedData.data.toFlowable(BackpressureStrategy.LATEST)) { data, state -> + disposables += store.update(observablePagedData.data.toFlowable(BackpressureStrategy.LATEST)) { data, state -> state.copy(storyContactItems = data) } } override fun onCleared() { disposables.clear() + store.dispose() } fun setStoriesEnabled(isEnabled: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt index db9116615f..b7cd12412e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt @@ -143,6 +143,7 @@ class StoryViewerViewModel( override fun onCleared() { disposables.clear() + store.dispose() } fun setSelectedPage(page: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryVolumeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryVolumeViewModel.kt index 8cbfd370d3..3e422577d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryVolumeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryVolumeViewModel.kt @@ -10,6 +10,10 @@ class StoryVolumeViewModel : ViewModel() { val state: Flowable = store.stateFlowable val snapshot: StoryVolumeState get() = store.state + override fun onCleared() { + store.dispose() + } + fun mute() { store.update { it.copy(isMuted = true) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt index c2d9c642a7..1ed2e043a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/info/StoryInfoViewModel.kt @@ -75,6 +75,7 @@ class StoryInfoViewModel(storyId: Long, repository: StoryInfoRepository = StoryI override fun onCleared() { disposables.clear() + store.dispose() } class Factory(private val storyId: Long) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt index 6b0136dc08..0601202476 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt @@ -99,6 +99,7 @@ class StoryViewerPageViewModel( override fun onCleared() { disposables.clear() storyCache.clear() + store.dispose() } fun hideStory(): Completable { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt index 81fd72e42d..be7861927e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt @@ -51,6 +51,7 @@ class StoryGroupReplyViewModel(storyId: Long, repository: StoryGroupReplyReposit override fun onCleared() { disposables.clear() + store.dispose() } class Factory(private val storyId: Long, private val repository: StoryGroupReplyRepository) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/rx/RxStore.kt b/app/src/main/java/org/thoughtcrime/securesms/util/rx/RxStore.kt index a36ced2e07..bab6d2cefa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/rx/RxStore.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/rx/RxStore.kt @@ -1,8 +1,10 @@ package org.thoughtcrime.securesms.util.rx +import androidx.annotation.CheckResult import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Scheduler import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.subjects.PublishSubject @@ -10,11 +12,14 @@ import io.reactivex.rxjava3.subjects.PublishSubject /** * Rx replacement for Store. * Actions are run on the computation thread by default. + * + * This class is disposable, and should be explicitly disposed of in a ViewModel's onCleared method + * to prevent memory leaks. Disposing instances of this class is a terminal action. */ class RxStore( defaultValue: T, scheduler: Scheduler = Schedulers.computation() -) { +) : Disposable { private val behaviorProcessor = BehaviorProcessor.createDefault(defaultValue) private val actionSubject = PublishSubject.create<(T) -> T>().toSerialized() @@ -22,20 +27,30 @@ class RxStore( val state: T get() = behaviorProcessor.value!! val stateFlowable: Flowable = behaviorProcessor.onBackpressureLatest() - init { - actionSubject - .observeOn(scheduler) - .scan(defaultValue) { v, f -> f(v) } - .subscribe { behaviorProcessor.onNext(it) } - } + val actionDisposable: Disposable = actionSubject + .observeOn(scheduler) + .scan(defaultValue) { v, f -> f(v) } + .subscribe { behaviorProcessor.onNext(it) } fun update(transformer: (T) -> T) { actionSubject.onNext(transformer) } - fun update(flowable: Flowable, transformer: (U, T) -> T): Disposable { + @CheckResult + fun update(flowable: Flowable, transformer: (U, T) -> T): Disposable { return flowable.subscribe { actionSubject.onNext { t -> transformer(it, t) } } } + + /** + * Dispose of the underlying scan chain. This is terminal. + */ + override fun dispose() { + actionDisposable.dispose() + } + + override fun isDisposed(): Boolean { + return actionDisposable.isDisposed + } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/rx/RxStoreTest.kt b/app/src/test/java/org/thoughtcrime/securesms/util/rx/RxStoreTest.kt index 2f97e577ed..3e70042a99 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/util/rx/RxStoreTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/util/rx/RxStoreTest.kt @@ -33,6 +33,7 @@ class RxStoreTest { // THEN subscriber.assertValueAt(0, 1) subscriber.assertNotComplete() + testSubject.dispose() } @Test @@ -50,6 +51,7 @@ class RxStoreTest { subscriber.assertValueAt(0, 1) subscriber.assertValueAt(1, 2) subscriber.assertNotComplete() + testSubject.dispose() } @Test @@ -66,5 +68,6 @@ class RxStoreTest { // THEN subscriber.assertValueAt(0, 2) subscriber.assertNotComplete() + testSubject.dispose() } }