diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/SplashDi.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/SplashDi.kt index 784dd274fc..444105aec5 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/SplashDi.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/SplashDi.kt @@ -5,6 +5,8 @@ import com.anytypeio.anytype.CrashReporter import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.core_utils.di.scope.PerScreen import com.anytypeio.anytype.core_utils.tools.FeatureToggles +import com.anytypeio.anytype.data.auth.event.EventProcessMigrationDateChannel +import com.anytypeio.anytype.data.auth.event.EventProcessMigrationRemoteChannel import com.anytypeio.anytype.di.common.ComponentDependencies import com.anytypeio.anytype.domain.account.AwaitAccountStartManager import com.anytypeio.anytype.domain.auth.interactor.CheckAuthorizationStatus @@ -31,7 +33,10 @@ import com.anytypeio.anytype.domain.search.RelationsSubscriptionManager import com.anytypeio.anytype.domain.spaces.SpaceDeletedStatusWatcher import com.anytypeio.anytype.domain.subscriptions.GlobalSubscriptionManager import com.anytypeio.anytype.domain.templates.GetTemplates +import com.anytypeio.anytype.domain.workspace.EventProcessMigrationChannel import com.anytypeio.anytype.domain.workspace.SpaceManager +import com.anytypeio.anytype.middleware.EventProxy +import com.anytypeio.anytype.middleware.interactor.EventProcessMigrationMiddlewareChannel import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate import com.anytypeio.anytype.presentation.splash.SplashViewModelFactory @@ -185,6 +190,18 @@ object SplashModule { fun bindMigrationHelperDelegate( impl: MigrationHelperDelegate.Impl ): MigrationHelperDelegate + + @Binds + @PerScreen + fun bindMigrationChannel( + impl: EventProcessMigrationDateChannel + ): EventProcessMigrationChannel + + @Binds + @PerScreen + fun bindMigrationRemoteChannel( + impl: EventProcessMigrationMiddlewareChannel + ): EventProcessMigrationRemoteChannel } } @@ -211,4 +228,5 @@ interface SplashDependencies : ComponentDependencies { fun globalSubscriptionManager(): GlobalSubscriptionManager fun logger(): Logger fun spaceViewSubscriptionContainer(): SpaceViewSubscriptionContainer + fun eventProxy(): EventProxy } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/login/OnboardingMnemonicLoginDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/login/OnboardingMnemonicLoginDI.kt index a2d8ee263c..18c6fb5ef1 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/login/OnboardingMnemonicLoginDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/login/OnboardingMnemonicLoginDI.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.ViewModelProvider import com.anytypeio.anytype.CrashReporter import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.core_utils.di.scope.PerScreen +import com.anytypeio.anytype.data.auth.event.EventProcessMigrationDateChannel +import com.anytypeio.anytype.data.auth.event.EventProcessMigrationRemoteChannel import com.anytypeio.anytype.di.common.ComponentDependencies import com.anytypeio.anytype.domain.account.AwaitAccountStartManager import com.anytypeio.anytype.domain.auth.repo.AuthRepository @@ -21,7 +23,10 @@ import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.platform.InitialParamsProvider import com.anytypeio.anytype.domain.spaces.SpaceDeletedStatusWatcher import com.anytypeio.anytype.domain.subscriptions.GlobalSubscriptionManager +import com.anytypeio.anytype.domain.workspace.EventProcessMigrationChannel import com.anytypeio.anytype.domain.workspace.SpaceManager +import com.anytypeio.anytype.middleware.EventProxy +import com.anytypeio.anytype.middleware.interactor.EventProcessMigrationMiddlewareChannel import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate import com.anytypeio.anytype.presentation.onboarding.login.OnboardingMnemonicLoginViewModel import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider @@ -81,6 +86,18 @@ object OnboardingMnemonicLoginModule { impl: MigrationHelperDelegate.Impl ): MigrationHelperDelegate + @Binds + @PerScreen + fun bindMigrationChannel( + impl: EventProcessMigrationDateChannel + ): EventProcessMigrationChannel + + @Binds + @PerScreen + fun bindMigrationRemoteChannel( + impl: EventProcessMigrationMiddlewareChannel + ): EventProcessMigrationRemoteChannel + @Binds @PerScreen fun bindViewModelFactory( @@ -108,4 +125,5 @@ interface OnboardingMnemonicLoginDependencies : ComponentDependencies { fun globalSubscriptionManager(): GlobalSubscriptionManager fun debugAccountSelectTrace(): DebugAccountSelectTrace fun logger(): Logger + fun eventProxy(): EventProxy } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/OnboardingRecoveryPhraseLoginScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/OnboardingRecoveryPhraseLoginScreen.kt index 6d2a6cc228..17e518adce 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/OnboardingRecoveryPhraseLoginScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/OnboardingRecoveryPhraseLoginScreen.kt @@ -228,7 +228,7 @@ fun RecoveryScreen( ) } is SetupState.Migration.InProgress -> { - MigrationInProgressScreen() + MigrationInProgressScreen(progress = state.progress.progress) } is SetupState.Migration.AwaitingStart -> { MigrationStartScreen( diff --git a/app/src/main/java/com/anytypeio/anytype/ui/splash/SplashFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/splash/SplashFragment.kt index fc1619ed03..fa34886205 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/splash/SplashFragment.kt @@ -90,7 +90,9 @@ class SplashFragment : BaseFragment(R.layout.fragment_spl ) } is SplashViewModel.State.Migration.InProgress -> { - MigrationInProgressScreen() + MigrationInProgressScreen( + progress = state.progress + ) } is SplashViewModel.State.Migration.Failed -> { MigrationFailedScreen( diff --git a/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorScreen.kt index badbbf2ba2..b802689131 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorScreen.kt @@ -1,5 +1,6 @@ package com.anytypeio.anytype.ui.update +import androidx.annotation.FloatRange import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -245,7 +246,9 @@ fun MigrationStartScreenPreview() { } @Composable -fun MigrationInProgressScreen() { +fun MigrationInProgressScreen( + @FloatRange(from = 0.0, to = 1.0) progress: Float +) { Box( modifier = Modifier .fillMaxSize() @@ -260,6 +263,7 @@ fun MigrationInProgressScreen() { .fillMaxWidth() ) { CircularProgressIndicator( + progress = { progress }, modifier = Modifier .size(88.dp) .align(Alignment.Center), @@ -370,7 +374,9 @@ fun MigrationFailedScreen( @DefaultPreviews @Composable fun MigrationInProgressScreenPreview() { - MigrationInProgressScreen() + MigrationInProgressScreen( + progress = 0.2f + ) } @DefaultPreviews diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Process.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Process.kt index f00616e798..326ae87a08 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Process.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Process.kt @@ -32,6 +32,12 @@ data class Process( sealed class Event { + sealed class Migration : Event() { + data class New(val process: Process) : Migration() + data class Update(val process: Process) : Migration() + data class Done(val process: Process) : Migration() + } + sealed class DropFiles : Event() { data class New( val process: Process diff --git a/data/build.gradle b/data/build.gradle index 97b7d0c219..242aa80a66 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -9,6 +9,8 @@ dependencies { implementation libs.kotlin implementation libs.coroutines + compileOnly libs.javaxInject + testImplementation project(":test:utils") testImplementation project(":test:core-models-stub") testImplementation libs.junit diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/event/EventProcessRemoteChannel.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/event/EventProcessRemoteChannel.kt index 2854f296ab..6ee97dabd4 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/event/EventProcessRemoteChannel.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/event/EventProcessRemoteChannel.kt @@ -3,6 +3,8 @@ package com.anytypeio.anytype.data.auth.event import com.anytypeio.anytype.core_models.Process import com.anytypeio.anytype.domain.workspace.EventProcessDropFilesChannel import com.anytypeio.anytype.domain.workspace.EventProcessImportChannel +import com.anytypeio.anytype.domain.workspace.EventProcessMigrationChannel +import javax.inject.Inject import kotlinx.coroutines.flow.Flow interface EventProcessImportRemoteChannel { @@ -29,4 +31,17 @@ class EventProcessDropFilesDateChannel( override fun observe(): Flow> { return channel.observe() } -} \ No newline at end of file +} + +interface EventProcessMigrationRemoteChannel { + fun observe(): Flow> +} + +class EventProcessMigrationDateChannel @Inject constructor( + private val channel: EventProcessMigrationRemoteChannel +) : EventProcessMigrationChannel { + + override fun observe(): Flow> { + return channel.observe() + } +} diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/auth/interactor/MigrateAccount.kt b/domain/src/main/java/com/anytypeio/anytype/domain/auth/interactor/MigrateAccount.kt index 5adecd3b0d..5d9477921f 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/auth/interactor/MigrateAccount.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/auth/interactor/MigrateAccount.kt @@ -37,4 +37,5 @@ class MigrateAccount @Inject constructor( data object Current : Params() data class Other(val acc: Id) : Params() } -} \ No newline at end of file +} + diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/workspace/EventProcessChannel.kt b/domain/src/main/java/com/anytypeio/anytype/domain/workspace/EventProcessChannel.kt index ff337b8dc1..5f1ede16d6 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/workspace/EventProcessChannel.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/workspace/EventProcessChannel.kt @@ -9,4 +9,8 @@ interface EventProcessImportChannel { interface EventProcessDropFilesChannel { fun observe(): Flow> +} + +interface EventProcessMigrationChannel { + fun observe(): Flow> } \ No newline at end of file diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/EventProcessMiddlewareChannel.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/EventProcessMiddlewareChannel.kt index 872ac7274e..7ced839eba 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/EventProcessMiddlewareChannel.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/EventProcessMiddlewareChannel.kt @@ -3,8 +3,10 @@ package com.anytypeio.anytype.middleware.interactor import com.anytypeio.anytype.core_models.Process import com.anytypeio.anytype.data.auth.event.EventProcessDropFilesRemoteChannel import com.anytypeio.anytype.data.auth.event.EventProcessImportRemoteChannel +import com.anytypeio.anytype.data.auth.event.EventProcessMigrationRemoteChannel import com.anytypeio.anytype.middleware.EventProxy import com.anytypeio.anytype.middleware.mappers.toCoreModel +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapNotNull @@ -113,6 +115,59 @@ class EventProcessImportMiddlewareChannel( } } + else -> null + } + } + } + } +} + +class EventProcessMigrationMiddlewareChannel @Inject constructor( + private val events: EventProxy +) : EventProcessMigrationRemoteChannel { + + override fun observe(): Flow> { + return events.flow() + .mapNotNull { emission -> + emission.messages.mapNotNull { message -> + val eventProcessNew = message.processNew + val eventProcessUpdate = message.processUpdate + val eventProcessDone = message.processDone + + when { + eventProcessNew != null -> { + val process = eventProcessNew.process + val processType = process?.migration + if (processType != null) { + Process.Event.Migration.New( + process = process.toCoreModel() + ) + } else { + null + } + } + eventProcessUpdate != null -> { + val process = eventProcessUpdate.process + val processType = process?.migration + if (processType != null) { + Process.Event.Migration.Update( + process = process.toCoreModel() + ) + } else { + null + } + } + eventProcessDone != null -> { + val process = eventProcessDone.process + val processType = process?.migration + if (processType != null) { + Process.Event.Migration.Done( + process = process.toCoreModel() + ) + } else { + null + } + } else -> null } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/auth/account/MigrationHelperDelegate.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/auth/account/MigrationHelperDelegate.kt index 9200c66273..a40a899e6f 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/auth/account/MigrationHelperDelegate.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/auth/account/MigrationHelperDelegate.kt @@ -1,13 +1,22 @@ package com.anytypeio.anytype.presentation.auth.account +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Process +import com.anytypeio.anytype.core_models.Process.Event import com.anytypeio.anytype.core_models.exceptions.MigrationFailedException import com.anytypeio.anytype.domain.auth.interactor.MigrateAccount import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.base.Resultat +import com.anytypeio.anytype.domain.workspace.EventProcessMigrationChannel import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.scan interface MigrationHelperDelegate { @@ -15,26 +24,36 @@ interface MigrationHelperDelegate { class Impl @Inject constructor( private val migrateAccount: MigrateAccount, - private val dispatchers: AppCoroutineDispatchers + private val dispatchers: AppCoroutineDispatchers, + private val processProgressObserver: MigrationProgressObserver ) : MigrationHelperDelegate { override suspend fun proceedWithMigration(): Flow { return migrateAccount .stream(MigrateAccount.Params.Current) - .map { result -> - when(result) { - is Resultat.Failure -> { - val exception = result.exception - if (exception is MigrationFailedException.NotEnoughSpace) { - State.Failed.NotEnoughSpace( - requiredSpaceInMegabytes = (exception.requiredSpaceInBytes / 1_048_576) - ) - } else { - State.Failed.UnknownError(result.exception) + .flatMapLatest { result -> + flow { + when(result) { + is Resultat.Failure -> { + val exception = result.exception + if (exception is MigrationFailedException.NotEnoughSpace) { + emit( + State.Failed.NotEnoughSpace( + requiredSpaceInMegabytes = (exception.requiredSpaceInBytes / 1_048_576) + ) + ) + } else { + emit(State.Failed.UnknownError(result.exception)) + } + } + is Resultat.Loading -> { + emitAll(processProgressObserver.state) + } + is Resultat.Success -> { + emit(State.Migrated) } } - is Resultat.Loading -> State.InProgress - is Resultat.Success -> State.Migrated + } } .flowOn(dispatchers.io) @@ -43,11 +62,79 @@ interface MigrationHelperDelegate { sealed class State { data object Init: State() - data object InProgress : State() + sealed class InProgress : State() { + abstract val progress: Float + data object Idle : InProgress() { + override val progress: Float = 0f + } + data class Progress(val processId: Id, override val progress: Float) : InProgress() + } sealed class Failed : State() { data class UnknownError(val error: Throwable) : Failed() data class NotEnoughSpace(val requiredSpaceInMegabytes: Long) : Failed() } data object Migrated : State() } +} + +typealias MigrationEvents = List + +class MigrationProgressObserver @Inject constructor( + private val channel: EventProcessMigrationChannel +) { + val state : Flow get() = channel + .observe() + .scan( + initial = MigrationHelperDelegate.State.InProgress.Idle + ) { state, events -> + var result = state + events.forEach { event -> + when (event) { + is Event.Migration.New -> { + if (result is MigrationHelperDelegate.State.InProgress.Idle && event.process.state == Process.State.RUNNING) { + result = MigrationHelperDelegate.State.InProgress.Progress( + processId = event.process.id, + progress = 0f + ) + } else { + // Some process is already running + } + } + is Event.Migration.Update -> { + val currentProgressState = result + val newProcess = event.process + if (currentProgressState is MigrationHelperDelegate.State.InProgress.Progress + && currentProgressState.processId == event.process.id + && newProcess.state == Process.State.RUNNING + ) { + val progress = newProcess.progress + val total = progress?.total + val done = progress?.done + result = + if (total != null && total != 0L && done != null) { + currentProgressState.copy( + progress = (done.toFloat() / total).coerceIn(0f, 1f) + ) + } else { + currentProgressState.copy(progress = 0f) + } + } + } + is Event.Migration.Done -> { + val currentProgressState = result + if (currentProgressState is MigrationHelperDelegate.State.InProgress.Progress + && event.process.state == Process.State.DONE + && event.process.id == currentProgressState.processId + ) { + result = MigrationHelperDelegate.State.Migrated + } + } + } + } + result + } + .distinctUntilChanged() + .catch { + emit(MigrationHelperDelegate.State.Failed.UnknownError(it)) + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/login/OnboardingMnemonicLoginViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/login/OnboardingMnemonicLoginViewModel.kt index 3052cf5fea..23b7f46632 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/login/OnboardingMnemonicLoginViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/login/OnboardingMnemonicLoginViewModel.kt @@ -318,15 +318,16 @@ class OnboardingMnemonicLoginViewModel @Inject constructor( account = id ) } - MigrationHelperDelegate.State.InProgress -> { + is MigrationHelperDelegate.State.InProgress -> { state.value = SetupState.Migration.InProgress( - account = id + account = id, + progress = migrationState ) } - MigrationHelperDelegate.State.Migrated -> { + is MigrationHelperDelegate.State.Migrated -> { proceedWithSelectingAccount(id) } - MigrationHelperDelegate.State.Init -> { + is MigrationHelperDelegate.State.Init -> { // Do nothing. } } @@ -435,7 +436,10 @@ class OnboardingMnemonicLoginViewModel @Inject constructor( sealed class Migration : SetupState() { abstract val account: Id data class AwaitingStart(override val account: Id) : Migration() - data class InProgress(override val account: Id): Migration() + data class InProgress( + override val account: Id, + val progress: MigrationHelperDelegate.State.InProgress + ): Migration() data class Failed( val state: MigrationHelperDelegate.State.Failed, override val account: Id diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/splash/SplashViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/splash/SplashViewModel.kt index 898625f3bd..851c39fa0f 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/splash/SplashViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/splash/SplashViewModel.kt @@ -17,11 +17,9 @@ import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectTypeIds.COLLECTION import com.anytypeio.anytype.core_models.SupportedLayouts import com.anytypeio.anytype.core_models.exceptions.AccountMigrationNeededException -import com.anytypeio.anytype.core_models.exceptions.MigrationFailedException import com.anytypeio.anytype.core_models.exceptions.NeedToUpdateApplicationException import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_models.primitives.TypeKey -import com.anytypeio.anytype.core_utils.ui.ViewState import com.anytypeio.anytype.domain.auth.interactor.CheckAuthorizationStatus import com.anytypeio.anytype.domain.auth.interactor.GetLastOpenedObject import com.anytypeio.anytype.domain.auth.interactor.LaunchAccount @@ -35,13 +33,11 @@ import com.anytypeio.anytype.domain.page.CreateObjectByTypeAndTemplate import com.anytypeio.anytype.domain.spaces.GetLastOpenedSpace import com.anytypeio.anytype.domain.subscriptions.GlobalSubscriptionManager import com.anytypeio.anytype.domain.workspace.SpaceManager -import com.anytypeio.anytype.presentation.BuildConfig import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate import com.anytypeio.anytype.presentation.confgs.ChatConfig import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectCreateEvent import com.anytypeio.anytype.presentation.search.ObjectSearchConstants -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flatMapLatest @@ -116,7 +112,9 @@ class SplashViewModel( // Do nothing. } is MigrationHelperDelegate.State.InProgress -> { - state.value = State.Migration.InProgress + state.value = State.Migration.InProgress( + progress = migrationState.progress + ) } is MigrationHelperDelegate.State.Migrated -> { proceedWithLaunchingAccount() @@ -455,7 +453,7 @@ class SplashViewModel( data class Error(val msg: String): State() sealed class Migration : State() { data object AwaitingStart: Migration() - data object InProgress: Migration() + data class InProgress(val progress: Float): Migration() data class Failed(val state: MigrationHelperDelegate.State.Failed) : Migration() } } diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/MigrationProgressObserverTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/MigrationProgressObserverTest.kt new file mode 100644 index 0000000000..4fa2c7a97d --- /dev/null +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/MigrationProgressObserverTest.kt @@ -0,0 +1,182 @@ +package com.anytypeio.anytype.presentation + +import app.cash.turbine.test +import com.anytypeio.anytype.core_models.Process.Event +import com.anytypeio.anytype.domain.workspace.EventProcessMigrationChannel +import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate +import com.anytypeio.anytype.presentation.auth.account.MigrationProgressObserver +import com.anytypeio.anytype.presentation.util.DefaultCoroutineTestRule +import kotlin.test.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.stub + +class MigrationProgressObserverTest { + + @OptIn(ExperimentalCoroutinesApi::class) + @get:Rule + val coroutineTestRule = DefaultCoroutineTestRule() + + @Mock + lateinit var channel: EventProcessMigrationChannel + + lateinit var observer: MigrationProgressObserver + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + observer = MigrationProgressObserver(channel) + } + + @Test + fun test() = runTest { + + val processId = "test-process" + val initEvent = Event.Migration.New( + process = com.anytypeio.anytype.core_models.Process( + id = processId, + spaceId = "space-id", + type = com.anytypeio.anytype.core_models.Process.Type.MIGRATION, + state = com.anytypeio.anytype.core_models.Process.State.RUNNING, + progress = null + ) + ) + + val updateEvent = Event.Migration.Update( + process = com.anytypeio.anytype.core_models.Process( + id = processId, + spaceId = "space-id", + type = com.anytypeio.anytype.core_models.Process.Type.MIGRATION, + state = com.anytypeio.anytype.core_models.Process.State.RUNNING, + progress = com.anytypeio.anytype.core_models.Process.Progress(done = 50, total = 100, message = "") + ) + ) + + val eventsFlow = flow { + emit(listOf(initEvent)) // Emit the initial event + emit(listOf(updateEvent)) // Emit the update event + } + + channel.stub { + on { observe() } doReturn eventsFlow + } + + observer.state.test { + // Test initial state + val firstState = awaitItem() // Should receive initial event's state + + assertEquals( + expected = MigrationHelperDelegate.State.InProgress.Idle, + actual = firstState + ) + + val secondState = awaitItem() + + assertEquals( + expected = MigrationHelperDelegate.State.InProgress.Progress( + processId = processId, + progress = 0f + ), + actual = secondState + ) + + // Test the updated state + val updatedState = awaitItem() // Should receive the update event's state + assert(updatedState is MigrationHelperDelegate.State.InProgress.Progress) + assert((updatedState as MigrationHelperDelegate.State.InProgress.Progress).progress == 0.5f) + + awaitComplete() // Ensure the flow completes properly + } + } + + @Test + fun testIdleState() = runTest { + + val eventsFlow = flow> { + emit(listOf()) // Emit the initial event with no progress + } + + channel.stub { + on { observe() } doReturn eventsFlow + } + + observer.state.test { + // Test initial state + val firstState = awaitItem() // Should receive initial event's state + assertEquals( + expected = MigrationHelperDelegate.State.InProgress.Idle, + actual = firstState + ) + + awaitComplete() // Ensure the flow completes properly + } + } + + @Test + fun testCompletedProgressState() = runTest { + val processId = "test-process" + val initEvent = Event.Migration.New( + process = com.anytypeio.anytype.core_models.Process( + id = processId, + spaceId = "space-id", + type = com.anytypeio.anytype.core_models.Process.Type.MIGRATION, + state = com.anytypeio.anytype.core_models.Process.State.RUNNING, + progress = null + ) + ) + + val completedEvent = Event.Migration.Done( + process = com.anytypeio.anytype.core_models.Process( + id = processId, + spaceId = "space-id", + type = com.anytypeio.anytype.core_models.Process.Type.MIGRATION, + state = com.anytypeio.anytype.core_models.Process.State.DONE, + progress = com.anytypeio.anytype.core_models.Process.Progress(done = 100, total = 100, message = "Migration complete") + ) + ) + + val eventsFlow = flow { + emit(listOf(initEvent)) // Emit the initial event + delay(100) // Simulate some delay + emit(listOf(completedEvent)) // Emit the completed event + } + + channel.stub { + on { observe() } doReturn eventsFlow + } + + observer.state.test { + // Test initial state + val firstState = awaitItem() // Should receive initial event's state + assertEquals( + expected = MigrationHelperDelegate.State.InProgress.Idle, + actual = firstState + ) + + val secondState = awaitItem() + + assertEquals( + expected = MigrationHelperDelegate.State.InProgress.Progress( + processId = processId, + progress = 0f + ), + actual = secondState + ) + + val completedState = awaitItem() + assertTrue(completedState is MigrationHelperDelegate.State.Migrated) + + awaitComplete() + + } + } +} \ No newline at end of file