Migrate RageshakeDetectionView to new architecture
This commit is contained in:
parent
acc091ef5f
commit
6d2e5edc82
6 changed files with 133 additions and 224 deletions
|
|
@ -25,7 +25,7 @@ import io.element.android.x.architecture.viewmodel.DaggerMavericksBindings
|
||||||
|
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
@MergeComponent(AppScope::class)
|
@MergeComponent(AppScope::class)
|
||||||
interface AppComponent : DaggerMavericksBindings, NodeFactoriesBindings {
|
interface AppComponent : NodeFactoriesBindings {
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,13 +16,11 @@
|
||||||
|
|
||||||
package io.element.android.x.features.rageshake.detection
|
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 takeScreenshot: Boolean = false,
|
||||||
val showDialog: Boolean = false,
|
val showDialog: Boolean = false,
|
||||||
val isEnabled: Boolean = true,
|
|
||||||
val isStarted: Boolean = false,
|
val isStarted: Boolean = false,
|
||||||
val isSupported: Boolean = false,
|
val preferenceState: RageshakePreferencesState = RageshakePreferencesState()
|
||||||
val sensitivity: Float = 0.5f,
|
)
|
||||||
) : MavericksState
|
|
||||||
|
|
@ -18,16 +18,11 @@ package io.element.android.x.features.rageshake.detection
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
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.LogCompositions
|
||||||
import io.element.android.x.core.compose.OnLifecycleEvent
|
|
||||||
import io.element.android.x.core.hardware.vibrate
|
import io.element.android.x.core.hardware.vibrate
|
||||||
import io.element.android.x.core.screenshot.ImageResult
|
import io.element.android.x.core.screenshot.ImageResult
|
||||||
import io.element.android.x.core.screenshot.screenshot
|
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
|
import io.element.android.x.element.resources.R as ElementR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RageshakeDetectionScreen(
|
fun RageshakeDetectionView(
|
||||||
viewModel: RageshakeDetectionViewModel = mavericksViewModel(),
|
state: RageshakeDetectionState,
|
||||||
onOpenBugReport: () -> Unit = { },
|
onOpenBugReport: () -> Unit = { },
|
||||||
|
onScreenshotTaken: (ImageResult) -> Unit,
|
||||||
|
onDisableClicked: () -> Unit,
|
||||||
|
onNoClicked: () -> Unit
|
||||||
) {
|
) {
|
||||||
val state: RageshakeDetectionViewState by viewModel.collectAsState()
|
|
||||||
LogCompositions(tag = "Rageshake", msg = "RageshakeDetectionScreen")
|
LogCompositions(tag = "Rageshake", msg = "RageshakeDetectionScreen")
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
OnLifecycleEvent { _, event ->
|
|
||||||
when (event) {
|
|
||||||
Lifecycle.Event.ON_RESUME -> viewModel.start()
|
|
||||||
Lifecycle.Event.ON_PAUSE -> viewModel.stop()
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when {
|
when {
|
||||||
state.takeScreenshot -> TakeScreenshot(
|
state.takeScreenshot -> TakeScreenshot(
|
||||||
onScreenshotTaken = viewModel::onScreenshotTaken
|
onScreenshotTaken = onScreenshotTaken
|
||||||
)
|
)
|
||||||
state.showDialog -> {
|
state.showDialog -> {
|
||||||
LaunchedEffect(key1 = "RS_diag") {
|
LaunchedEffect(key1 = "RS_diag") {
|
||||||
|
|
@ -61,14 +50,9 @@ fun RageshakeDetectionScreen(
|
||||||
}
|
}
|
||||||
RageshakeDialogContent(
|
RageshakeDialogContent(
|
||||||
state,
|
state,
|
||||||
onNoClicked = viewModel::onNo,
|
onNoClicked = onNoClicked,
|
||||||
onDisableClicked = {
|
onDisableClicked = onDisableClicked,
|
||||||
viewModel.onEnableClicked(false)
|
onYesClicked = onOpenBugReport
|
||||||
},
|
|
||||||
onYesClicked = {
|
|
||||||
onOpenBugReport()
|
|
||||||
viewModel.onYes()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +70,7 @@ private fun TakeScreenshot(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RageshakeDialogContent(
|
fun RageshakeDialogContent(
|
||||||
state: RageshakeDetectionViewState,
|
state: RageshakeDetectionState,
|
||||||
onNoClicked: () -> Unit = { },
|
onNoClicked: () -> Unit = { },
|
||||||
onDisableClicked: () -> Unit = { },
|
onDisableClicked: () -> Unit = { },
|
||||||
onYesClicked: () -> Unit = { },
|
onYesClicked: () -> Unit = { },
|
||||||
|
|
@ -108,7 +92,7 @@ fun RageshakeDialogContent(
|
||||||
fun RageshakeDialogContentPreview() {
|
fun RageshakeDialogContentPreview() {
|
||||||
ElementXTheme {
|
ElementXTheme {
|
||||||
RageshakeDialogContent(
|
RageshakeDialogContent(
|
||||||
state = RageshakeDetectionViewState()
|
state = RageshakeDetectionState()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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…
Add table
Add a link
Reference in a new issue