From 9a4a9a294ff2ff03f90a7ae785c091ba86185c0d Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sat, 25 Feb 2023 17:55:48 +0100 Subject: [PATCH] Add handler for launch app gesture action --- .../ui/launcher/LauncherScaffoldVM.kt | 96 ++++++++++++++----- .../ui/launcher/SharedLauncherActivity.kt | 51 +--------- .../gestures/LauncherGestureHandler.kt | 87 +++++++++++++++++ 3 files changed, 163 insertions(+), 71 deletions(-) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/gestures/LauncherGestureHandler.kt diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldVM.kt index 78571a76..43545dc7 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldVM.kt @@ -1,6 +1,6 @@ package de.mm20.launcher2.ui.launcher -import android.util.Log +import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -8,19 +8,23 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope +import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.globalactions.GlobalActionsService import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.Settings -import de.mm20.launcher2.preferences.Settings.GestureSettings import de.mm20.launcher2.preferences.Settings.GestureSettings.GestureAction +import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.ui.gestures.Gesture import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -30,17 +34,7 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent { private val dataStore: LauncherDataStore by inject() private val globalActionsService: GlobalActionsService by inject() private val permissionsManager: PermissionsManager by inject() - - private var gestureSettings : GestureSettings? = null - - - init { - viewModelScope.launch { - dataStore.data.map { it.gestures }.collectLatest { - gestureSettings = it - } - } - } + private val favoritesRepository: FavoritesRepository by inject() private var isSystemInDarkMode = MutableStateFlow(false) @@ -112,15 +106,46 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent { val searchBarStyle = dataStore.data.map { it.searchBar.searchBarStyle }.asLiveData() - val shouldDetectDoubleTapGesture = dataStore.data.map { it.gestures.doubleTap != GestureAction.None }.asLiveData() + val gestureState: StateFlow = dataStore + .data.map { it.gestures } + .distinctUntilChanged() + .map { settings -> + val swipeLeftAction = settings?.swipeLeft ?: GestureAction.None + val swipeRightAction = settings?.swipeRight ?: GestureAction.None + val swipeDownAction = settings?.swipeDown ?: GestureAction.None + val longPressAction = settings?.longPress ?: GestureAction.None + val doubleTapAction = settings?.doubleTap ?: GestureAction.None + val apps = listOfNotNull( + if (swipeLeftAction == GestureAction.LaunchApp) settings.swipeLeftApp else null, + if (swipeRightAction == GestureAction.LaunchApp) settings.swipeRightApp else null, + if (swipeDownAction == GestureAction.LaunchApp) settings.swipeDownApp else null, + if (longPressAction == GestureAction.LaunchApp) settings.longPressApp else null, + if (doubleTapAction == GestureAction.LaunchApp) settings.doubleTapApp else null + ).let { favoritesRepository.getFromKeys(it) } + + GestureState( + swipeLeftAction = swipeLeftAction, + swipeRightAction = swipeRightAction, + swipeDownAction = swipeDownAction, + longPressAction = longPressAction, + doubleTapAction = doubleTapAction, + swipeLeftApp = apps.firstOrNull { it.key == settings?.swipeLeftApp }, + swipeRightApp = apps.firstOrNull { it.key == settings?.swipeRightApp }, + swipeDownApp = apps.firstOrNull { it.key == settings?.swipeDownApp }, + longPressApp = apps.firstOrNull { it.key == settings?.longPressApp }, + doubleTapApp = apps.firstOrNull { it.key == settings?.doubleTapApp } + ) + }.stateIn(viewModelScope, SharingStarted.Eagerly, GestureState()) + + var failedGestureState by mutableStateOf(null) - fun handleGesture(gesture: Gesture): Boolean { + fun handleGesture(context: Context, gesture: Gesture): Boolean { val action = when (gesture) { - Gesture.DoubleTap -> gestureSettings?.doubleTap - Gesture.LongPress -> gestureSettings?.longPress - Gesture.SwipeDown -> gestureSettings?.swipeDown?.takeIf { baseLayout.value != Settings.LayoutSettings.Layout.PullDown } - Gesture.SwipeLeft -> gestureSettings?.swipeLeft?.takeIf { baseLayout.value != Settings.LayoutSettings.Layout.Pager } - Gesture.SwipeRight -> gestureSettings?.swipeRight?.takeIf { baseLayout.value != Settings.LayoutSettings.Layout.PagerReversed } + Gesture.DoubleTap -> gestureState.value.doubleTapAction + Gesture.LongPress -> gestureState.value.longPressAction + Gesture.SwipeDown -> gestureState.value.swipeDownAction?.takeIf { baseLayout.value != Settings.LayoutSettings.Layout.PullDown } + Gesture.SwipeLeft -> gestureState.value.swipeLeftAction?.takeIf { baseLayout.value != Settings.LayoutSettings.Layout.Pager } + Gesture.SwipeRight -> gestureState.value.swipeRightAction?.takeIf { baseLayout.value != Settings.LayoutSettings.Layout.PagerReversed } } val requiresAccessibilityService = action == GestureAction.OpenRecents @@ -129,7 +154,10 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent { || action == GestureAction.OpenNotificationDrawer || action == GestureAction.LockScreen - if (action != null && requiresAccessibilityService && !permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)) { + if (action != null && requiresAccessibilityService && !permissionsManager.checkPermissionOnce( + PermissionGroup.Accessibility + ) + ) { failedGestureState = FailedGesture(gesture, action) return true } @@ -166,6 +194,17 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent { true } + GestureAction.LaunchApp -> { + when (gesture) { + Gesture.SwipeLeft -> gestureState.value.swipeLeftApp + Gesture.SwipeRight -> gestureState.value.swipeRightApp + Gesture.SwipeDown -> gestureState.value.swipeDownApp + Gesture.LongPress -> gestureState.value.longPressApp + Gesture.DoubleTap -> gestureState.value.doubleTapApp + }?.launch(context, null) + true + } + else -> false } } @@ -175,4 +214,17 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent { } } +data class GestureState( + val swipeLeftAction: GestureAction = GestureAction.None, + val swipeRightAction: GestureAction = GestureAction.None, + val swipeDownAction: GestureAction = GestureAction.None, + val longPressAction: GestureAction = GestureAction.None, + val doubleTapAction: GestureAction = GestureAction.None, + val swipeLeftApp: SavableSearchable? = null, + val swipeRightApp: SavableSearchable? = null, + val swipeDownApp: SavableSearchable? = null, + val longPressApp: SavableSearchable? = null, + val doubleTapApp: SavableSearchable? = null, +) + data class FailedGesture(val gesture: Gesture, val action: GestureAction) \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt index 9581e395..df708034 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt @@ -53,6 +53,7 @@ import de.mm20.launcher2.ui.gestures.GestureHandler import de.mm20.launcher2.ui.gestures.LocalGestureDetector import de.mm20.launcher2.ui.ktx.animateTo import de.mm20.launcher2.ui.ktx.toPixels +import de.mm20.launcher2.ui.launcher.gestures.LauncherGestureHandler import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.sheets.FailedGestureSheet import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheets @@ -97,10 +98,6 @@ abstract class SharedLauncherActivity( val bottomSheetManager = LauncherBottomSheetManager() val gestureDetector = GestureDetector() - viewModel.shouldDetectDoubleTapGesture.observe(this) { - gestureDetector.shouldDetectDoubleTaps = it - } - setContent { val snackbarHostState = remember { SnackbarHostState() } val wallpaperColors by wallpaperColorsAsState() @@ -270,51 +267,7 @@ abstract class SharedLauncherActivity( } LauncherBottomSheets() } - - val swipeThreshold = 150.dp.toPixels() - GestureHandler( - detector = gestureDetector, - onDoubleTap = { - viewModel.handleGesture(Gesture.DoubleTap) - }, - onLongPress = { - viewModel.handleGesture(Gesture.LongPress) - }, - onDrag = { - return@GestureHandler when { - it.x > swipeThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> { - viewModel.handleGesture(Gesture.SwipeRight) - } - - it.x < -swipeThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> { - viewModel.handleGesture(Gesture.SwipeLeft) - } - - it.y > swipeThreshold && it.y.absoluteValue > it.x.absoluteValue * 2f -> { - viewModel.handleGesture(Gesture.SwipeDown) - } - else -> false - } - }, - onTap = { - wallpaperManager.sendWallpaperCommand( - window.decorView.windowToken, - WallpaperManager.COMMAND_TAP, - it.x.toInt(), - it.y.toInt(), - 0, - null - ) - } - ) - if (viewModel.failedGestureState != null) { - FailedGestureSheet( - failedGesture = viewModel.failedGestureState!!, - onDismiss = { - viewModel.dismissGestureFailedSheet() - } - ) - } + LauncherGestureHandler() } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/gestures/LauncherGestureHandler.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/gestures/LauncherGestureHandler.kt new file mode 100644 index 00000000..4be220c5 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/gestures/LauncherGestureHandler.kt @@ -0,0 +1,87 @@ +package de.mm20.launcher2.ui.launcher.gestures + +import android.app.WallpaperManager +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.preferences.Settings.GestureSettings.GestureAction +import de.mm20.launcher2.ui.gestures.Gesture +import de.mm20.launcher2.ui.gestures.GestureHandler +import de.mm20.launcher2.ui.gestures.LocalGestureDetector +import de.mm20.launcher2.ui.ktx.toPixels +import de.mm20.launcher2.ui.launcher.GestureState +import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM +import de.mm20.launcher2.ui.launcher.sheets.FailedGestureSheet +import kotlin.math.absoluteValue + +@Composable +fun LauncherGestureHandler() { + val context = LocalContext.current + val wallpaperManager = remember { WallpaperManager.getInstance(context) } + val gestureDetector = LocalGestureDetector.current + + val viewModel: LauncherScaffoldVM = viewModel() + + val gestureState by viewModel.gestureState.collectAsState(GestureState()) + + val shouldDetectDoubleTapGesture = gestureState.gdoubleTapAction != GestureAction.None + + LaunchedEffect(shouldDetectDoubleTapGesture) { + gestureDetector.shouldDetectDoubleTaps = shouldDetectDoubleTapGesture + } + + val windowToken = LocalView.current.windowToken + + + val swipeThreshold = 150.dp.toPixels() + GestureHandler( + detector = gestureDetector, + onDoubleTap = { + viewModel.handleGesture(context, Gesture.DoubleTap) + }, + onLongPress = { + viewModel.handleGesture(context, Gesture.LongPress) + }, + onDrag = { + return@GestureHandler when { + it.x > swipeThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> { + viewModel.handleGesture(context, Gesture.SwipeRight) + } + + it.x < -swipeThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> { + viewModel.handleGesture(context, Gesture.SwipeLeft) + } + + it.y > swipeThreshold && it.y.absoluteValue > it.x.absoluteValue * 2f -> { + viewModel.handleGesture(context, Gesture.SwipeDown) + } + + else -> false + } + }, + onTap = { + wallpaperManager.sendWallpaperCommand( + windowToken, + WallpaperManager.COMMAND_TAP, + it.x.toInt(), + it.y.toInt(), + 0, + null + ) + } + ) + if (viewModel.failedGestureState != null) { + FailedGestureSheet( + failedGesture = viewModel.failedGestureState!!, + onDismiss = { + viewModel.dismissGestureFailedSheet() + } + ) + } +} \ No newline at end of file