ganfra
2 years ago
6 changed files with 133 additions and 224 deletions
@ -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 |
||||||
|
} |
@ -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<RageshakeDetectionState, RageshakeDetectionEvents> { |
||||||
|
|
||||||
|
private val preferencesEventsFlow = SharedFlowHolder<RageshakePreferencesEvents>() |
||||||
|
|
||||||
|
@Composable |
||||||
|
override fun present(events: Flow<RageshakeDetectionEvents>): 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<Boolean>) { |
||||||
|
if (start) { |
||||||
|
rageShake.start(state.preferenceState.sensitivity) |
||||||
|
rageShake.interceptor = { |
||||||
|
takeScreenshot.value = true |
||||||
|
} |
||||||
|
} else { |
||||||
|
rageShake.stop() |
||||||
|
rageShake.interceptor = null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun CoroutineScope.processScreenshot(takeScreenshot: MutableState<Boolean>, showDialog: MutableState<Boolean>, 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 |
||||||
|
} |
||||||
|
} |
@ -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<RageshakeDetectionViewState>(initialState) { |
|
||||||
|
|
||||||
companion object : |
|
||||||
MavericksViewModelFactory<RageshakeDetectionViewModel, RageshakeDetectionViewState> 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) |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue