diff options
author | 2024-12-27 16:48:18 +0100 | |
---|---|---|
committer | 2025-01-03 17:10:49 +0000 | |
commit | 150e45970e8023747c0dca1df2db05121a6e1650 (patch) | |
tree | 39a9e8bbec8108d31d67938976d5c558f7e38179 | |
parent | d00d46e22dae09b398a452fb8ecc0321c34dcd1b (diff) |
1/n refactoring: splitting ViewModels into separate smaller classes
Splitting ViewModels into smaller more cohesive classes with extracted common logic:
- GestureRecognizerAdapter transforms callback based GestureRecognizer into Flow and substitutes GestureFlowAdapter
- GestureRecognizerProvider which hides complexity of observing changes in resources and refreshing GestureRecognizer
- TouchpadGestureResources extracts common code for accessing latest values for distance and velocity
In that new class structure ViewModels responsibility is only to transform business state into Ui state that is to collect latest GestureState from GestureRecognizerAdapter and transform it into GestureUiState.
Bug: 384509663
Test: all unit tests
Flag: com.android.systemui.shared.new_touchpad_gestures_tutorial
Change-Id: I2da7cbeb530a1d4de91746d22b8126fd74e7154b
19 files changed, 432 insertions, 174 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt index 79c1f9fcf517..4aec88e8497b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt @@ -16,12 +16,13 @@ package com.android.systemui.touchpad.tutorial.ui.viewmodel +import android.content.res.mockResources import android.view.MotionEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository -import com.android.systemui.common.ui.domain.interactor.configurationInteractor +import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest @@ -34,18 +35,27 @@ import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finis import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.InProgress import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE import com.android.systemui.touchpad.tutorial.ui.gesture.ThreeFingerGesture +import com.android.systemui.touchpad.ui.gesture.touchpadGestureResources import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class BackGestureScreenViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() + private val resources = kosmos.mockResources private val fakeConfigRepository = kosmos.fakeConfigurationRepository - private val viewModel = BackGestureScreenViewModel(kosmos.configurationInteractor) + private val viewModel = + BackGestureScreenViewModel( + GestureRecognizerAdapter( + BackGestureRecognizerProvider(kosmos.touchpadGestureResources), + kosmos.inputDeviceTutorialLogger, + ) + ) @Before fun before() { @@ -115,10 +125,12 @@ class BackGestureScreenViewModelTest : SysuiTestCase() { } private fun setThresholdResource(threshold: Float) { - fakeConfigRepository.setDimensionPixelSize( - R.dimen.touchpad_tutorial_gestures_distance_threshold, - (threshold).toInt(), - ) + whenever( + resources.getDimensionPixelSize( + R.dimen.touchpad_tutorial_gestures_distance_threshold + ) + ) + .thenReturn(threshold.toInt()) fakeConfigRepository.onAnyConfigurationChange() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModelTest.kt index 4af374287c62..8bd796b5c851 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModelTest.kt @@ -20,6 +20,7 @@ import android.view.MotionEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest @@ -36,7 +37,13 @@ import org.junit.runner.RunWith class EasterEggGestureViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() - private val viewModel = EasterEggGestureViewModel() + private val viewModel = + EasterEggGestureViewModel( + GestureRecognizerAdapter( + EasterEggRecognizerProvider(), + kosmos.inputDeviceTutorialLogger, + ) + ) @Before fun before() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt index 4dfd01a91f17..65a995dcd043 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt @@ -22,7 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository -import com.android.systemui.common.ui.domain.interactor.configurationInteractor +import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest @@ -37,6 +37,7 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Comp import com.android.systemui.touchpad.tutorial.ui.gesture.ThreeFingerGesture import com.android.systemui.touchpad.tutorial.ui.gesture.Velocity import com.android.systemui.touchpad.ui.gesture.fakeVelocityTracker +import com.android.systemui.touchpad.ui.gesture.touchpadGestureResources import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -59,7 +60,12 @@ class HomeGestureScreenViewModelTest : SysuiTestCase() { private val resources = kosmos.mockResources private val viewModel = - HomeGestureScreenViewModel(kosmos.configurationInteractor, resources, fakeVelocityTracker) + HomeGestureScreenViewModel( + GestureRecognizerAdapter( + HomeGestureRecognizerProvider(kosmos.touchpadGestureResources, fakeVelocityTracker), + kosmos.inputDeviceTutorialLogger, + ) + ) @Before fun before() { @@ -126,10 +132,12 @@ class HomeGestureScreenViewModelTest : SysuiTestCase() { } private fun setDistanceThreshold(threshold: Float) { - fakeConfigRepository.setDimensionPixelSize( - R.dimen.touchpad_tutorial_gestures_distance_threshold, - (threshold).toInt(), - ) + whenever( + resources.getDimensionPixelSize( + R.dimen.touchpad_tutorial_gestures_distance_threshold + ) + ) + .thenReturn(threshold.toInt()) fakeConfigRepository.onAnyConfigurationChange() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt index 66bf778a754b..1bc60b67095e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt @@ -22,7 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository -import com.android.systemui.common.ui.domain.interactor.configurationInteractor +import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest @@ -37,6 +37,7 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Comp import com.android.systemui.touchpad.tutorial.ui.gesture.ThreeFingerGesture import com.android.systemui.touchpad.tutorial.ui.gesture.Velocity import com.android.systemui.touchpad.ui.gesture.fakeVelocityTracker +import com.android.systemui.touchpad.ui.gesture.touchpadGestureResources import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -60,9 +61,13 @@ class RecentAppsGestureScreenViewModelTest : SysuiTestCase() { private val viewModel = RecentAppsGestureScreenViewModel( - kosmos.configurationInteractor, - resources, - fakeVelocityTracker, + GestureRecognizerAdapter( + RecentAppsGestureRecognizerProvider( + kosmos.touchpadGestureResources, + fakeVelocityTracker, + ), + kosmos.inputDeviceTutorialLogger, + ) ) @Before diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt index a6c066500054..c43f31beb5bc 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt @@ -30,9 +30,15 @@ import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialS import com.android.systemui.touchpad.tutorial.ui.gesture.VelocityTracker import com.android.systemui.touchpad.tutorial.ui.gesture.VerticalVelocityTracker import com.android.systemui.touchpad.tutorial.ui.view.TouchpadTutorialActivity +import com.android.systemui.touchpad.tutorial.ui.viewmodel.BackGestureRecognizerProvider import com.android.systemui.touchpad.tutorial.ui.viewmodel.BackGestureScreenViewModel import com.android.systemui.touchpad.tutorial.ui.viewmodel.EasterEggGestureViewModel +import com.android.systemui.touchpad.tutorial.ui.viewmodel.EasterEggRecognizerProvider +import com.android.systemui.touchpad.tutorial.ui.viewmodel.GestureRecognizerAdapter +import com.android.systemui.touchpad.tutorial.ui.viewmodel.HomeGestureRecognizerProvider import com.android.systemui.touchpad.tutorial.ui.viewmodel.HomeGestureScreenViewModel +import com.android.systemui.touchpad.tutorial.ui.viewmodel.RecentAppsGestureRecognizerProvider +import com.android.systemui.touchpad.tutorial.ui.viewmodel.RecentAppsGestureScreenViewModel import dagger.Binds import dagger.Module import dagger.Provides @@ -53,14 +59,46 @@ interface TouchpadTutorialModule { fun touchpadScreensProvider( backGestureScreenViewModel: BackGestureScreenViewModel, homeGestureScreenViewModel: HomeGestureScreenViewModel, + easterEggGestureViewModel: EasterEggGestureViewModel, ): TouchpadTutorialScreensProvider { return ScreensProvider( backGestureScreenViewModel, homeGestureScreenViewModel, - EasterEggGestureViewModel(), + easterEggGestureViewModel, ) } + @Provides + fun recentAppsViewModel( + recognizerProvider: RecentAppsGestureRecognizerProvider, + adapterFactory: GestureRecognizerAdapter.Factory, + ): RecentAppsGestureScreenViewModel { + return RecentAppsGestureScreenViewModel(adapterFactory.create(recognizerProvider)) + } + + @Provides + fun backViewModel( + recognizerProvider: BackGestureRecognizerProvider, + adapterFactory: GestureRecognizerAdapter.Factory, + ): BackGestureScreenViewModel { + return BackGestureScreenViewModel(adapterFactory.create(recognizerProvider)) + } + + @Provides + fun homeViewModel( + recognizerProvider: HomeGestureRecognizerProvider, + adapterFactory: GestureRecognizerAdapter.Factory, + ): HomeGestureScreenViewModel { + return HomeGestureScreenViewModel(adapterFactory.create(recognizerProvider)) + } + + @Provides + fun easterEggViewModel( + adapterFactory: GestureRecognizerAdapter.Factory + ): EasterEggGestureViewModel { + return EasterEggGestureViewModel(adapterFactory.create(EasterEggRecognizerProvider())) + } + @SysUISingleton @Provides fun touchpadGesturesInteractor( diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadEventsFilter.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadEventsFilter.kt index bddeb0b25ec2..b4b8ff0a3949 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadEventsFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadEventsFilter.kt @@ -20,6 +20,7 @@ import android.view.InputDevice import android.view.MotionEvent import android.view.MotionEvent.ACTION_DOWN import android.view.MotionEvent.BUTTON_PRIMARY +import com.android.systemui.touchpad.tutorial.ui.viewmodel.GestureRecognizerAdapter object TouchpadEventsFilter { @@ -42,3 +43,12 @@ fun GestureRecognizer.handleTouchpadMotionEvent(event: MotionEvent): Boolean { false } } + +fun GestureRecognizerAdapter.handleTouchpadMotionEvent(event: MotionEvent): Boolean { + return if (TouchpadEventsFilter.isTouchpadAndNonClickEvent(event)) { + this.accept(event) + true + } else { + false + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt index cefe382a299c..3264300ed908 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt @@ -58,6 +58,7 @@ constructor( private val backGestureViewModel: BackGestureScreenViewModel, private val homeGestureViewModel: HomeGestureScreenViewModel, private val recentAppsGestureViewModel: RecentAppsGestureScreenViewModel, + private val easterEggGestureViewModel: EasterEggGestureViewModel, ) : ComponentActivity() { private val tutorialViewModel by @@ -74,7 +75,7 @@ constructor( backGestureViewModel, homeGestureViewModel, recentAppsGestureViewModel, - EasterEggGestureViewModel(), + easterEggGestureViewModel, closeTutorial = ::finishTutorial, ) } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureRecognizerProvider.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureRecognizerProvider.kt new file mode 100644 index 000000000000..b089882e9cf8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureRecognizerProvider.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 com.android.systemui.touchpad.tutorial.ui.viewmodel + +import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureRecognizer +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class BackGestureRecognizerProvider @Inject constructor(val resources: TouchpadGestureResources) : + GestureRecognizerProvider { + + override val recognizer: Flow<GestureRecognizer> = + resources.distanceThreshold().map { distance -> + BackGestureRecognizer(gestureDistanceThresholdPx = distance) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt index 93e8d313edcf..8e53669a7841 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt @@ -17,47 +17,26 @@ package com.android.systemui.touchpad.tutorial.ui.viewmodel import android.view.MotionEvent -import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState -import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureRecognizer import com.android.systemui.touchpad.tutorial.ui.gesture.GestureDirection -import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent import com.android.systemui.util.kotlin.pairwiseBy -import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -class BackGestureScreenViewModel -@Inject -constructor(configurationInteractor: ConfigurationInteractor) : TouchpadTutorialScreenViewModel { +class BackGestureScreenViewModel(val gestureRecognizer: GestureRecognizerAdapter) : + TouchpadTutorialScreenViewModel { - private var recognizer: BackGestureRecognizer? = null - - private val distanceThreshold: Flow<Int> = - configurationInteractor - .dimensionPixelSize(R.dimen.touchpad_tutorial_gestures_distance_threshold) - .distinctUntilChanged() - - @OptIn(ExperimentalCoroutinesApi::class) override val gestureUiState: Flow<GestureUiState> = - distanceThreshold - .flatMapLatest { - recognizer = BackGestureRecognizer(gestureDistanceThresholdPx = it) - GestureFlowAdapter(recognizer!!).gestureStateAsFlow - } - .pairwiseBy(GestureState.NotStarted) { previous, current -> - toGestureUiState(current, previous) - } + gestureRecognizer.gestureState.pairwiseBy(GestureState.NotStarted) { previous, current -> + toGestureUiState(current, previous) + } override fun handleEvent(event: MotionEvent): Boolean { - return recognizer?.handleTouchpadMotionEvent(event) ?: false + return gestureRecognizer.handleTouchpadMotionEvent(event) } private fun toGestureUiState(current: GestureState, previous: GestureState): GestureUiState { diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModel.kt index 69cdab6108ab..9ca456dff58a 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModel.kt @@ -17,8 +17,6 @@ package com.android.systemui.touchpad.tutorial.ui.viewmodel import android.view.MotionEvent -import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureRecognizer -import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent import java.util.function.Consumer @@ -29,14 +27,10 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.receiveAsFlow -class EasterEggGestureViewModel( - private val gestureRecognizer: EasterEggGestureRecognizer = EasterEggGestureRecognizer() -) : Consumer<MotionEvent> { +class EasterEggGestureViewModel(val gestureRecognizer: GestureRecognizerAdapter) : + Consumer<MotionEvent> { - private val gestureDone = - GestureFlowAdapter(gestureRecognizer).gestureStateAsFlow.filter { - it == GestureState.Finished - } + private val gestureDone = gestureRecognizer.gestureState.filter { it == GestureState.Finished } private val easterEggFinished = Channel<Unit>() diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureFlowAdapter.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggRecognizerProvider.kt index 23e31b0a9efd..c48ccb52ba0f 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureFlowAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggRecognizerProvider.kt @@ -14,17 +14,15 @@ * limitations under the License. */ -package com.android.systemui.touchpad.tutorial.ui.gesture +package com.android.systemui.touchpad.tutorial.ui.viewmodel -import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow -import kotlinx.coroutines.channels.awaitClose +import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureRecognizer +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow -class GestureFlowAdapter(gestureRecognizer: GestureRecognizer) { +class EasterEggRecognizerProvider : GestureRecognizerProvider { - val gestureStateAsFlow: Flow<GestureState> = conflatedCallbackFlow { - val callback: (GestureState) -> Unit = { trySend(it) } - gestureRecognizer.addGestureStateCallback(callback) - awaitClose { gestureRecognizer.clearGestureStateCallback() } - } + override val recognizer: Flow<GestureRecognizer> = + MutableStateFlow(EasterEggGestureRecognizer()) } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/GestureRecognizerAdapter.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/GestureRecognizerAdapter.kt new file mode 100644 index 000000000000..8e7375fa923b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/GestureRecognizerAdapter.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 com.android.systemui.touchpad.tutorial.ui.viewmodel + +import android.view.MotionEvent +import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.util.function.Consumer +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest + +/** + * Adapter for [GestureRecognizer] exposing [GestureState] as Flow and ensuring that motion events + * are always handled by latest [GestureRecognizer]. + */ +class GestureRecognizerAdapter +@AssistedInject +constructor( + @Assisted provider: GestureRecognizerProvider, + private val logger: InputDeviceTutorialLogger, +) : Consumer<MotionEvent> { + + private var gestureRecognizer: GestureRecognizer? = null + + @OptIn(ExperimentalCoroutinesApi::class) + val gestureState: Flow<GestureState> = + provider.recognizer.flatMapLatest { + gestureRecognizer = it + gestureStateAsFlow(it) + } + + override fun accept(event: MotionEvent) { + if (gestureRecognizer == null) { + logger.w("sending MotionEvent before gesture recognizer is initialized") + } else { + gestureRecognizer?.accept(event) + } + } + + private fun gestureStateAsFlow(recognizer: GestureRecognizer): Flow<GestureState> = + conflatedCallbackFlow { + val callback: (GestureState) -> Unit = { trySend(it) } + recognizer.addGestureStateCallback(callback) + awaitClose { recognizer.clearGestureStateCallback() } + } + + @AssistedFactory + interface Factory { + fun create(provider: GestureRecognizerProvider): GestureRecognizerAdapter + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/GestureRecognizerProvider.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/GestureRecognizerProvider.kt new file mode 100644 index 000000000000..585bc0c425d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/GestureRecognizerProvider.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 com.android.systemui.touchpad.tutorial.ui.viewmodel + +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer +import kotlinx.coroutines.flow.Flow + +/** Observes state of the system and provides always up-to-date [GestureRecognizer] */ +interface GestureRecognizerProvider { + val recognizer: Flow<GestureRecognizer> +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureRecognizerProvider.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureRecognizerProvider.kt new file mode 100644 index 000000000000..6d818a6a0398 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureRecognizerProvider.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 com.android.systemui.touchpad.tutorial.ui.viewmodel + +import com.android.systemui.res.R +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer +import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureRecognizer +import com.android.systemui.touchpad.tutorial.ui.gesture.VelocityTracker +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map + +class HomeGestureRecognizerProvider +@Inject +constructor(val resources: TouchpadGestureResources, val velocityTracker: VelocityTracker) : + GestureRecognizerProvider { + + override val recognizer: Flow<GestureRecognizer> = + resources + .distanceThreshold() + .combine( + resources.velocityThreshold(R.dimen.touchpad_home_gesture_velocity_threshold), + { distance, velocity -> distance to velocity }, + ) + .map { (distance, velocity) -> + HomeGestureRecognizer( + gestureDistanceThresholdPx = distance, + velocityThresholdPxPerMs = velocity, + velocityTracker = velocityTracker, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt index 9a817d810bba..9d6f568fa1b1 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt @@ -16,70 +16,27 @@ package com.android.systemui.touchpad.tutorial.ui.viewmodel -import android.content.res.Resources import android.view.MotionEvent -import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState -import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter -import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState -import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureRecognizer -import com.android.systemui.touchpad.tutorial.ui.gesture.VelocityTracker -import com.android.systemui.touchpad.tutorial.ui.gesture.VerticalVelocityTracker import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent -import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map -class HomeGestureScreenViewModel -@Inject -constructor( - val configurationInteractor: ConfigurationInteractor, - @Main val resources: Resources, - val velocityTracker: VelocityTracker = VerticalVelocityTracker(), -) : TouchpadTutorialScreenViewModel { +class HomeGestureScreenViewModel(private val gestureRecognizer: GestureRecognizerAdapter) : + TouchpadTutorialScreenViewModel { - private var recognizer: HomeGestureRecognizer? = null - - private val distanceThreshold: Flow<Int> = - configurationInteractor - .dimensionPixelSize(R.dimen.touchpad_tutorial_gestures_distance_threshold) - .distinctUntilChanged() - - private val velocityThreshold: Flow<Float> = - configurationInteractor.onAnyConfigurationChange - .map { resources.getDimension(R.dimen.touchpad_home_gesture_velocity_threshold) } - .distinctUntilChanged() - - @OptIn(ExperimentalCoroutinesApi::class) override val gestureUiState: Flow<GestureUiState> = - distanceThreshold - .combine(velocityThreshold, { distance, velocity -> distance to velocity }) - .flatMapLatest { (distance, velocity) -> - recognizer = - HomeGestureRecognizer( - gestureDistanceThresholdPx = distance, - velocityThresholdPxPerMs = velocity, - velocityTracker = velocityTracker, - ) - GestureFlowAdapter(recognizer!!).gestureStateAsFlow - } - .map { toGestureUiState(it) } - - private fun toGestureUiState(it: GestureState) = - it.toGestureUiState( - progressStartMarker = "drag with gesture", - progressEndMarker = "release playback realtime", - successAnimation = R.raw.trackpad_home_success, - ) + gestureRecognizer.gestureState.map { + it.toGestureUiState( + progressStartMarker = "drag with gesture", + progressEndMarker = "release playback realtime", + successAnimation = R.raw.trackpad_home_success, + ) + } override fun handleEvent(event: MotionEvent): Boolean { - return recognizer?.handleTouchpadMotionEvent(event) ?: false + return gestureRecognizer.handleTouchpadMotionEvent(event) } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureRecognizerProvider.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureRecognizerProvider.kt new file mode 100644 index 000000000000..3e0b434036e5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureRecognizerProvider.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 com.android.systemui.touchpad.tutorial.ui.viewmodel + +import com.android.systemui.res.R +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer +import com.android.systemui.touchpad.tutorial.ui.gesture.RecentAppsGestureRecognizer +import com.android.systemui.touchpad.tutorial.ui.gesture.VelocityTracker +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map + +class RecentAppsGestureRecognizerProvider +@Inject +constructor(val resources: TouchpadGestureResources, val velocityTracker: VelocityTracker) : + GestureRecognizerProvider { + + override val recognizer: Flow<GestureRecognizer> = + resources + .distanceThreshold() + .combine( + resources.velocityThreshold( + R.dimen.touchpad_recent_apps_gesture_velocity_threshold + ), + { distance, velocity -> distance to velocity }, + ) + .map { (distance, velocity) -> + RecentAppsGestureRecognizer( + gestureDistanceThresholdPx = distance, + velocityThresholdPxPerMs = velocity, + velocityTracker = velocityTracker, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt index 8215078c346d..97528583277f 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt @@ -16,74 +16,27 @@ package com.android.systemui.touchpad.tutorial.ui.viewmodel -import android.content.res.Resources import android.view.MotionEvent -import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState -import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter -import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState -import com.android.systemui.touchpad.tutorial.ui.gesture.RecentAppsGestureRecognizer -import com.android.systemui.touchpad.tutorial.ui.gesture.VelocityTracker -import com.android.systemui.touchpad.tutorial.ui.gesture.VerticalVelocityTracker import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent -import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map -class RecentAppsGestureScreenViewModel -@Inject -constructor( - configurationInteractor: ConfigurationInteractor, - @Main private val resources: Resources, - private val velocityTracker: VelocityTracker = VerticalVelocityTracker(), -) : TouchpadTutorialScreenViewModel { +class RecentAppsGestureScreenViewModel(private val gestureRecognizer: GestureRecognizerAdapter) : + TouchpadTutorialScreenViewModel { - private var recognizer: RecentAppsGestureRecognizer? = null - - private val distanceThreshold: Flow<Int> = - configurationInteractor.onAnyConfigurationChange - .map { - resources.getDimensionPixelSize( - R.dimen.touchpad_tutorial_gestures_distance_threshold - ) - } - .distinctUntilChanged() - - private val velocityThreshold: Flow<Float> = - configurationInteractor.onAnyConfigurationChange - .map { resources.getDimension(R.dimen.touchpad_recent_apps_gesture_velocity_threshold) } - .distinctUntilChanged() - - @OptIn(ExperimentalCoroutinesApi::class) override val gestureUiState: Flow<GestureUiState> = - distanceThreshold - .combine(velocityThreshold, { distance, velocity -> distance to velocity }) - .flatMapLatest { (distance, velocity) -> - recognizer = - RecentAppsGestureRecognizer( - gestureDistanceThresholdPx = distance, - velocityThresholdPxPerMs = velocity, - velocityTracker = velocityTracker, - ) - GestureFlowAdapter(recognizer!!).gestureStateAsFlow - } - .map { toGestureUiState(it) } - - private fun toGestureUiState(it: GestureState) = - it.toGestureUiState( - progressStartMarker = "drag with gesture", - progressEndMarker = "onPause", - successAnimation = R.raw.trackpad_recent_apps_success, - ) + gestureRecognizer.gestureState.map { + it.toGestureUiState( + progressStartMarker = "drag with gesture", + progressEndMarker = "onPause", + successAnimation = R.raw.trackpad_recent_apps_success, + ) + } override fun handleEvent(event: MotionEvent): Boolean { - return recognizer?.handleTouchpadMotionEvent(event) ?: false + return gestureRecognizer.handleTouchpadMotionEvent(event) } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadGestureResources.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadGestureResources.kt new file mode 100644 index 000000000000..3d99bd82d79e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadGestureResources.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 com.android.systemui.touchpad.tutorial.ui.viewmodel + +import android.content.res.Resources +import androidx.annotation.DimenRes +import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.res.R +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +class TouchpadGestureResources +@Inject +constructor(val configurationInteractor: ConfigurationInteractor, @Main val resources: Resources) { + + fun distanceThreshold(): Flow<Int> = + configurationInteractor.onAnyConfigurationChange + .map { + resources.getDimensionPixelSize( + R.dimen.touchpad_tutorial_gestures_distance_threshold + ) + } + .distinctUntilChanged() + + fun velocityThreshold(@DimenRes resId: Int): Flow<Float> = + configurationInteractor.onAnyConfigurationChange + .map { resources.getDimension(resId) } + .distinctUntilChanged() +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/ui/gesture/TouchpadGestureResourcesKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/ui/gesture/TouchpadGestureResourcesKosmos.kt new file mode 100644 index 000000000000..d795941f96aa --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/ui/gesture/TouchpadGestureResourcesKosmos.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 com.android.systemui.touchpad.ui.gesture + +import android.content.res.mockResources +import com.android.systemui.common.ui.domain.interactor.configurationInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.touchpad.tutorial.ui.viewmodel.TouchpadGestureResources + +var Kosmos.touchpadGestureResources: TouchpadGestureResources by + Kosmos.Fixture { TouchpadGestureResources(configurationInteractor, mockResources) } |