From c299ab4031b7931fbb996020314a6cb766e6b5bc Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 10 Jan 2023 10:01:23 +0100 Subject: [PATCH] Migrate RageshakeDetectionView to new architecture --- .../io/element/android/x/di/AppComponent.kt | 2 +- .../detection/RageshakeDetectionEvents.kt | 11 + .../detection/RageshakeDetectionPresenter.kt | 106 ++++++++++ ...iewState.kt => RageshakeDetectionState.kt} | 10 +- ...ionScreen.kt => RageshakeDetectionView.kt} | 38 +--- .../detection/RageshakeDetectionViewModel.kt | 190 ------------------ 6 files changed, 133 insertions(+), 224 deletions(-) create mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt create mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt rename features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/{RageshakeDetectionViewState.kt => RageshakeDetectionState.kt} (79%) rename features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/{RageshakeDetectionScreen.kt => RageshakeDetectionView.kt} (72%) delete mode 100644 features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewModel.kt diff --git a/app/src/main/java/io/element/android/x/di/AppComponent.kt b/app/src/main/java/io/element/android/x/di/AppComponent.kt index 3320a88218..30f801e31d 100644 --- a/app/src/main/java/io/element/android/x/di/AppComponent.kt +++ b/app/src/main/java/io/element/android/x/di/AppComponent.kt @@ -25,7 +25,7 @@ import io.element.android.x.architecture.viewmodel.DaggerMavericksBindings @SingleIn(AppScope::class) @MergeComponent(AppScope::class) -interface AppComponent : DaggerMavericksBindings, NodeFactoriesBindings { +interface AppComponent : NodeFactoriesBindings { @Component.Factory interface Factory { diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt new file mode 100644 index 0000000000..e47c8040fa --- /dev/null +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt @@ -0,0 +1,11 @@ +package io.element.android.x.features.rageshake.detection + +import io.element.android.x.core.screenshot.ImageResult + +sealed interface RageshakeDetectionEvents { + object Dismiss: RageshakeDetectionEvents + object Disable : RageshakeDetectionEvents + object StartDetection : RageshakeDetectionEvents + object StopDetection : RageshakeDetectionEvents + data class ProcessScreenshot(val imageResult: ImageResult) : RageshakeDetectionEvents +} diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt new file mode 100644 index 0000000000..a9e14a476b --- /dev/null +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt @@ -0,0 +1,106 @@ +package io.element.android.x.features.rageshake.detection + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.x.architecture.Presenter +import io.element.android.x.architecture.SharedFlowHolder +import io.element.android.x.core.screenshot.ImageResult +import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents +import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter +import io.element.android.x.features.rageshake.rageshake.RageShake +import io.element.android.x.features.rageshake.rageshake.RageshakeDataStore +import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +class RageshakeDetectionPresenter @Inject constructor( + private val rageshakeDataStore: RageshakeDataStore, + private val screenshotHolder: ScreenshotHolder, + private val rageShake: RageShake, + private val preferencesPresenter: RageshakePreferencesPresenter, +) : Presenter { + + private val preferencesEventsFlow = SharedFlowHolder() + + @Composable + override fun present(events: Flow): RageshakeDetectionState { + val preferencesState = preferencesPresenter.present(events = preferencesEventsFlow.asSharedFlow()) + val isStarted = rememberSaveable { + mutableStateOf(false) + } + val takeScreenshot = rememberSaveable { + mutableStateOf(false) + } + val showDialog = rememberSaveable { + mutableStateOf(false) + } + val state = RageshakeDetectionState( + isStarted = isStarted.value, + takeScreenshot = takeScreenshot.value, + showDialog = showDialog.value, + preferenceState = preferencesState + ) + LaunchedEffect(Unit) { + events.collect { event -> + when (event) { + RageshakeDetectionEvents.Disable -> preferencesEventsFlow.emit(RageshakePreferencesEvents.SetIsEnabled(false)) + RageshakeDetectionEvents.StartDetection -> isStarted.value = true + RageshakeDetectionEvents.StopDetection -> isStarted.value = false + is RageshakeDetectionEvents.ProcessScreenshot -> processScreenshot(takeScreenshot, showDialog, event.imageResult) + RageshakeDetectionEvents.Dismiss -> showDialog.value = false + } + } + } + LaunchedEffect(preferencesState.sensitivity) { + rageShake.setSensitivity(preferencesState.sensitivity) + } + val shouldStart = remember { + derivedStateOf { + preferencesState.isEnabled && + preferencesState.isSupported && + isStarted.value && + !takeScreenshot.value && + !showDialog.value + } + } + LaunchedEffect(shouldStart) { + handleRageShake(shouldStart.value, state, takeScreenshot) + } + return state + } + + + private fun handleRageShake(start: Boolean, state: RageshakeDetectionState, takeScreenshot: MutableState) { + if (start) { + rageShake.start(state.preferenceState.sensitivity) + rageShake.interceptor = { + takeScreenshot.value = true + } + } else { + rageShake.stop() + rageShake.interceptor = null + } + } + + private fun CoroutineScope.processScreenshot(takeScreenshot: MutableState, showDialog: MutableState, imageResult: ImageResult) = launch { + screenshotHolder.reset() + when (imageResult) { + is ImageResult.Error -> { + Timber.e(imageResult.exception, "Unable to write screenshot") + } + is ImageResult.Success -> { + screenshotHolder.writeBitmap(imageResult.data) + } + } + takeScreenshot.value = false + showDialog.value = true + } +} diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewState.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt similarity index 79% rename from features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewState.kt rename to features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt index 4e426a079e..247c81a2dc 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewState.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt @@ -16,13 +16,11 @@ package io.element.android.x.features.rageshake.detection -import com.airbnb.mvrx.MavericksState +import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState -data class RageshakeDetectionViewState( +data class RageshakeDetectionState( val takeScreenshot: Boolean = false, val showDialog: Boolean = false, - val isEnabled: Boolean = true, val isStarted: Boolean = false, - val isSupported: Boolean = false, - val sensitivity: Float = 0.5f, -) : MavericksState + val preferenceState: RageshakePreferencesState = RageshakePreferencesState() +) diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionScreen.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt similarity index 72% rename from features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionScreen.kt rename to features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt index 576f4e7a92..02273df57e 100644 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionScreen.kt +++ b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt @@ -18,16 +18,11 @@ package io.element.android.x.features.rageshake.detection import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.Lifecycle -import com.airbnb.mvrx.compose.collectAsState -import com.airbnb.mvrx.compose.mavericksViewModel import io.element.android.x.core.compose.LogCompositions -import io.element.android.x.core.compose.OnLifecycleEvent import io.element.android.x.core.hardware.vibrate import io.element.android.x.core.screenshot.ImageResult import io.element.android.x.core.screenshot.screenshot @@ -36,24 +31,18 @@ import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog import io.element.android.x.element.resources.R as ElementR @Composable -fun RageshakeDetectionScreen( - viewModel: RageshakeDetectionViewModel = mavericksViewModel(), +fun RageshakeDetectionView( + state: RageshakeDetectionState, onOpenBugReport: () -> Unit = { }, + onScreenshotTaken: (ImageResult) -> Unit, + onDisableClicked: () -> Unit, + onNoClicked: () -> Unit ) { - val state: RageshakeDetectionViewState by viewModel.collectAsState() LogCompositions(tag = "Rageshake", msg = "RageshakeDetectionScreen") val context = LocalContext.current - OnLifecycleEvent { _, event -> - when (event) { - Lifecycle.Event.ON_RESUME -> viewModel.start() - Lifecycle.Event.ON_PAUSE -> viewModel.stop() - else -> Unit - } - } - when { state.takeScreenshot -> TakeScreenshot( - onScreenshotTaken = viewModel::onScreenshotTaken + onScreenshotTaken = onScreenshotTaken ) state.showDialog -> { LaunchedEffect(key1 = "RS_diag") { @@ -61,14 +50,9 @@ fun RageshakeDetectionScreen( } RageshakeDialogContent( state, - onNoClicked = viewModel::onNo, - onDisableClicked = { - viewModel.onEnableClicked(false) - }, - onYesClicked = { - onOpenBugReport() - viewModel.onYes() - } + onNoClicked = onNoClicked, + onDisableClicked = onDisableClicked, + onYesClicked = onOpenBugReport ) } } @@ -86,7 +70,7 @@ private fun TakeScreenshot( @Composable fun RageshakeDialogContent( - state: RageshakeDetectionViewState, + state: RageshakeDetectionState, onNoClicked: () -> Unit = { }, onDisableClicked: () -> Unit = { }, onYesClicked: () -> Unit = { }, @@ -108,7 +92,7 @@ fun RageshakeDialogContent( fun RageshakeDialogContentPreview() { ElementXTheme { RageshakeDialogContent( - state = RageshakeDetectionViewState() + state = RageshakeDetectionState() ) } } diff --git a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewModel.kt b/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewModel.kt deleted file mode 100644 index e3ccde755d..0000000000 --- a/features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewModel.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.x.features.rageshake.detection - -import com.airbnb.mvrx.MavericksViewModel -import com.airbnb.mvrx.MavericksViewModelFactory -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.x.anvilannotations.ContributesViewModel -import io.element.android.x.architecture.viewmodel.daggerMavericksViewModelFactory -import io.element.android.x.core.screenshot.ImageResult -import io.element.android.x.di.AppScope -import io.element.android.x.features.rageshake.rageshake.RageShake -import io.element.android.x.features.rageshake.rageshake.RageshakeDataStore -import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import timber.log.Timber - -@ContributesViewModel(AppScope::class) -class RageshakeDetectionViewModel @AssistedInject constructor( - @Assisted initialState: RageshakeDetectionViewState, - private val rageshakeDataStore: RageshakeDataStore, - private val screenshotHolder: ScreenshotHolder, - private val rageShake: RageShake, -) : MavericksViewModel(initialState) { - - companion object : - MavericksViewModelFactory by daggerMavericksViewModelFactory() - - init { - setState { - copy( - isSupported = rageShake.isAvailable() - ) - } - observeDataStore() - observeState() - } - - private fun observeDataStore() { - viewModelScope.launch { - rageshakeDataStore.isEnabled().collect { isEnabled -> - setState { - copy( - isEnabled = isEnabled - ) - } - } - } - viewModelScope.launch { - rageshakeDataStore.sensitivity().collect { sensitivity -> - setState { - copy( - sensitivity = sensitivity - ) - } - } - } - } - - private fun observeState() { - viewModelScope.launch { - stateFlow - .map { - it.isSupported && - it.isEnabled && - it.isStarted && - !it.takeScreenshot && - !it.showDialog - } - .distinctUntilChanged() - .collect(::handleRageShake) - } - viewModelScope.launch { - stateFlow - .map { - it.sensitivity - } - .distinctUntilChanged() - .collect { - rageShake.setSensitivity(it) - } - } - } - - private fun handleRageShake(shouldStart: Boolean) { - if (shouldStart) { - withState { - rageShake.start(it.sensitivity) - } - rageShake.interceptor = { - setState { - copy( - takeScreenshot = true - ) - } - } - } else { - rageShake.stop() - rageShake.interceptor = null - } - } - - fun onScreenshotTaken(imageResult: ImageResult) { - viewModelScope.launch(Dispatchers.IO) { - screenshotHolder.reset() - when (imageResult) { - is ImageResult.Error -> { - Timber.e(imageResult.exception, "Unable to write screenshot") - } - is ImageResult.Success -> { - screenshotHolder.writeBitmap(imageResult.data) - } - } - setState { - copy( - takeScreenshot = false, - showDialog = true, - ) - } - } - } - - fun start() { - setState { - copy(isStarted = true) - } - } - - private fun onPopupDismissed() { - setState { - copy( - showDialog = false - ) - } - } - - fun onNo() { - onPopupDismissed() - } - - fun onYes() { - onPopupDismissed() - } - - fun onEnableClicked(enabled: Boolean) { - viewModelScope.launch { - rageshakeDataStore.setIsEnabled(enabled) - } - if (!enabled) { - onPopupDismissed() - } - } - - fun onSensitivityChange(sensitivity: Float) { - viewModelScope.launch { - rageshakeDataStore.setSensitivity(sensitivity) - } - rageShake.setSensitivity(sensitivity) - } - - fun stop() { - setState { - copy(isStarted = false) - } - } - - override fun onCleared() { - super.onCleared() - stop() - handleRageShake(false) - } -}