Browse Source

Migrate RageshakeDetectionView to new architecture

feature/bma/flipper
ganfra 2 years ago
parent
commit
c299ab4031
  1. 2
      app/src/main/java/io/element/android/x/di/AppComponent.kt
  2. 11
      features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt
  3. 106
      features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt
  4. 10
      features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt
  5. 38
      features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionView.kt
  6. 190
      features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewModel.kt

2
app/src/main/java/io/element/android/x/di/AppComponent.kt

@ -25,7 +25,7 @@ import io.element.android.x.architecture.viewmodel.DaggerMavericksBindings @@ -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 {

11
features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionEvents.kt

@ -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
}

106
features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionPresenter.kt

@ -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
}
}

10
features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewState.kt → features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionState.kt

@ -16,13 +16,11 @@ @@ -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()
)

38
features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionScreen.kt → 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 @@ -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 @@ -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( @@ -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( @@ -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( @@ -108,7 +92,7 @@ fun RageshakeDialogContent(
fun RageshakeDialogContentPreview() {
ElementXTheme {
RageshakeDialogContent(
state = RageshakeDetectionViewState()
state = RageshakeDetectionState()
)
}
}

190
features/rageshake/src/main/java/io/element/android/x/features/rageshake/detection/RageshakeDetectionViewModel.kt

@ -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…
Cancel
Save