ganfra
2 years ago
6 changed files with 133 additions and 224 deletions
@ -0,0 +1,11 @@
@@ -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 @@
@@ -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 @@
@@ -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