From c12c07fa455e974d351bde0999db3c3fe348ae14 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Tue, 17 Jan 2023 18:55:30 +0100 Subject: [PATCH] Gestures --- app/app/build.gradle.kts | 1 + .../de/mm20/launcher2/LauncherApplication.kt | 2 + app/ui/build.gradle.kts | 1 + .../de/mm20/launcher2/ui/gestures/Gesture.kt | 9 ++ .../ui/launcher/LauncherScaffoldVM.kt | 90 +++++++++++- .../ui/launcher/SharedLauncherActivity.kt | 49 +++---- .../ui/launcher/sheets/FailedGestureSheet.kt | 79 +++++++++++ .../launcher/sheets/FailedGestureSheetVM.kt | 40 ++++++ .../launcher/sheets/LauncherBottomSheets.kt | 16 +++ .../launcher2/ui/settings/SettingsActivity.kt | 4 + .../gestures/GestureSettingsScreen.kt | 131 ++++++++++++++++++ .../gestures/GestureSettingsScreenVM.kt | 78 +++++++++++ .../ui/settings/main/MainSettingsScreen.kt | 8 ++ core/i18n/src/main/res/values/strings.xml | 17 +++ .../permissions/PermissionsManager.kt | 39 ++++++ .../de/mm20/launcher2/preferences/Defaults.kt | 42 +++--- .../preferences/migrations/Migration_11_12.kt | 9 ++ .../preferences/src/main/proto/settings.proto | 18 +++ services/global-actions/.gitignore | 1 + services/global-actions/build.gradle.kts | 47 +++++++ services/global-actions/consumer-rules.pro | 0 services/global-actions/proguard-rules.pro | 21 +++ .../src/main/AndroidManifest.xml | 17 +++ .../globalactions/GlobalActionsService.kt | 25 ++++ .../LauncherAccessibilityService.kt | 40 ++++++ .../de/mm20/launcher2/globalactions/Module.kt | 7 + .../main/res/xml/accessibility_service.xml | 5 + settings.gradle.kts | 1 + 28 files changed, 752 insertions(+), 45 deletions(-) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/gestures/Gesture.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/FailedGestureSheet.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/FailedGestureSheetVM.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreenVM.kt create mode 100644 services/global-actions/.gitignore create mode 100644 services/global-actions/build.gradle.kts create mode 100644 services/global-actions/consumer-rules.pro create mode 100644 services/global-actions/proguard-rules.pro create mode 100644 services/global-actions/src/main/AndroidManifest.xml create mode 100644 services/global-actions/src/main/java/de/mm20/launcher2/globalactions/GlobalActionsService.kt create mode 100644 services/global-actions/src/main/java/de/mm20/launcher2/globalactions/LauncherAccessibilityService.kt create mode 100644 services/global-actions/src/main/java/de/mm20/launcher2/globalactions/Module.kt create mode 100644 services/global-actions/src/main/res/xml/accessibility_service.xml diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 63de2d9d..64cff06b 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -137,6 +137,7 @@ dependencies { implementation(project(":data:wikipedia")) implementation(project(":core:database")) implementation(project(":data:search-actions")) + implementation(project(":services:global-actions")) // Uncomment this if you want annoying notifications in your debug builds yelling at you how terrible your code is //debugImplementation(libs.leakcanary) diff --git a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt index 7d2ac72d..d8a1a794 100644 --- a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt +++ b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt @@ -24,6 +24,7 @@ import de.mm20.launcher2.websites.websitesModule import de.mm20.launcher2.widgets.widgetsModule import de.mm20.launcher2.wikipedia.wikipediaModule import de.mm20.launcher2.database.databaseModule +import de.mm20.launcher2.globalactions.globalActionsModule import de.mm20.launcher2.notifications.notificationsModule import de.mm20.launcher2.permissions.permissionsModule import de.mm20.launcher2.preferences.preferencesModule @@ -64,6 +65,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory { databaseModule, favoritesModule, filesModule, + globalActionsModule, iconsModule, musicModule, notificationsModule, diff --git a/app/ui/build.gradle.kts b/app/ui/build.gradle.kts index 649ccada..7fc8b096 100644 --- a/app/ui/build.gradle.kts +++ b/app/ui/build.gradle.kts @@ -143,4 +143,5 @@ dependencies { implementation(project(":services:accounts")) implementation(project(":services:backup")) implementation(project(":data:search-actions")) + implementation(project(":services:global-actions")) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/gestures/Gesture.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/gestures/Gesture.kt new file mode 100644 index 00000000..dfedd6d5 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/gestures/Gesture.kt @@ -0,0 +1,9 @@ +package de.mm20.launcher2.ui.gestures + +enum class Gesture { + DoubleTap, + LongPress, + SwipeDown, + SwipeLeft, + SwipeRight, +} \ No newline at end of file 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 330142e4..226f9c6f 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,12 +1,23 @@ package de.mm20.launcher2.ui.launcher +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope +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.ui.gestures.Gesture import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -17,6 +28,19 @@ import org.koin.core.component.inject 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 var isSystemInDarkMode = MutableStateFlow(false) @@ -84,4 +108,68 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent { val fillClockHeight = dataStore.data.map { it.clockWidget.fillHeight }.asLiveData() val searchBarColor = dataStore.data.map { it.searchBar.color }.asLiveData() val searchBarStyle = dataStore.data.map { it.searchBar.searchBarStyle }.asLiveData() -} \ No newline at end of file + + + var failedGestureState by mutableStateOf(null) + fun handleGesture(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 } + } + val requiresAccessibilityService = + action == GestureAction.OpenRecents + || action == GestureAction.OpenPowerDialog + || action == GestureAction.OpenQuickSettings + || action == GestureAction.OpenNotificationDrawer + || action == GestureAction.LockScreen + + if (action != null && requiresAccessibilityService && !permissionsManager.checkPermissionOnce(PermissionGroup.Accessibility)) { + failedGestureState = FailedGesture(gesture, action) + return true + } + + + return when (action) { + GestureAction.OpenSearch -> { + openSearch() + true + } + + GestureAction.OpenNotificationDrawer -> { + globalActionsService.openNotificationDrawer() + true + } + + GestureAction.OpenQuickSettings -> { + globalActionsService.openQuickSettings() + true + } + + GestureAction.LockScreen -> { + globalActionsService.lockScreen() + true + } + + GestureAction.OpenPowerDialog -> { + globalActionsService.openPowerDialog() + true + } + + GestureAction.OpenRecents -> { + globalActionsService.openRecents() + true + } + + else -> false + } + } + + fun dismissGestureFailedSheet() { + failedGestureState = 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 ad4082be..7b040c94 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 @@ -4,7 +4,6 @@ import android.app.WallpaperManager import android.content.res.Configuration import android.content.res.Resources import android.os.Bundle -import android.util.Log import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.foundation.background @@ -37,6 +36,7 @@ import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import com.google.accompanist.systemuicontroller.rememberSystemUiController +import de.mm20.launcher2.globalactions.GlobalActionsService import de.mm20.launcher2.preferences.Settings import de.mm20.launcher2.preferences.Settings.SystemBarsSettings.SystemBarColors import de.mm20.launcher2.ui.assistant.AssistantScaffold @@ -44,13 +44,14 @@ import de.mm20.launcher2.ui.base.BaseActivity import de.mm20.launcher2.ui.base.ProvideCurrentTime import de.mm20.launcher2.ui.base.ProvideSettings import de.mm20.launcher2.ui.component.NavBarEffects +import de.mm20.launcher2.ui.gestures.Gesture import de.mm20.launcher2.ui.gestures.GestureDetector 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.sheets.CustomizeSearchableSheet -import de.mm20.launcher2.ui.launcher.sheets.EditFavoritesSheet +import de.mm20.launcher2.ui.launcher.sheets.FailedGestureSheet +import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheets import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheetManager import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import de.mm20.launcher2.ui.launcher.transitions.HomeTransition @@ -62,6 +63,7 @@ import de.mm20.launcher2.ui.locals.LocalWallpaperColors import de.mm20.launcher2.ui.locals.LocalWindowSize import de.mm20.launcher2.ui.theme.LauncherTheme import de.mm20.launcher2.ui.theme.wallpaperColorsAsState +import org.koin.android.ext.android.inject import kotlin.math.absoluteValue import kotlin.math.pow @@ -70,7 +72,9 @@ abstract class SharedLauncherActivity( private val mode: LauncherActivityMode ) : BaseActivity() { - private val viewModel: LauncherActivityVM by viewModels() + private val viewModel: LauncherScaffoldVM by viewModels() + + private val globalActionsService: GlobalActionsService by inject() internal val homeTransitionManager = HomeTransitionManager() @@ -256,52 +260,43 @@ abstract class SharedLauncherActivity( ) { enterTransitionProgress.value } } } - - bottomSheetManager.customizeSearchableSheetShown.value?.let { - CustomizeSearchableSheet( - searchable = it, - onDismiss = { bottomSheetManager.dismissCustomizeSearchableModal() }) - } - if (bottomSheetManager.editFavoritesSheetShown.value) { - EditFavoritesSheet(onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() }) - } + LauncherBottomSheets() } val swipeThreshold = 150.dp.toPixels() GestureHandler( detector = gestureDetector, onDoubleTap = { - Log.d("MM20", "Double tap") + viewModel.handleGesture(Gesture.DoubleTap) }, onLongPress = { - Log.d("MM20", "Long press") + viewModel.handleGesture(Gesture.LongPress) }, onDrag = { return@GestureHandler when { it.x > swipeThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> { - Log.d("MM20", "Swipe right") - true + viewModel.handleGesture(Gesture.SwipeRight) } it.x < -swipeThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> { - Log.d("MM20", "Swipe left") - true + viewModel.handleGesture(Gesture.SwipeLeft) } it.y > swipeThreshold && it.y.absoluteValue > it.x.absoluteValue * 2f -> { - Log.d("MM20", "Swipe down") - true + viewModel.handleGesture(Gesture.SwipeDown) } - - it.y < -swipeThreshold && it.y.absoluteValue > it.x.absoluteValue * 2f -> { - Log.d("MM20", "Swipe up") - true - } - else -> false } } ) + if (viewModel.failedGestureState != null) { + FailedGestureSheet( + failedGesture = viewModel.failedGestureState!!, + onDismiss = { + viewModel.dismissGestureFailedSheet() + } + ) + } } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/FailedGestureSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/FailedGestureSheet.kt new file mode 100644 index 00000000..5412d52b --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/FailedGestureSheet.kt @@ -0,0 +1,79 @@ +package de.mm20.launcher2.ui.launcher.sheets + +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.preferences.Settings +import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.BottomSheetDialog +import de.mm20.launcher2.ui.component.MissingPermissionBanner +import de.mm20.launcher2.ui.gestures.Gesture +import de.mm20.launcher2.ui.launcher.FailedGesture + +@Composable +fun FailedGestureSheet( + failedGesture: FailedGesture, + onDismiss: () -> Unit, +) { + val viewModel: FailedGestureSheetVM = viewModel() + + val actionName = stringResource(when(failedGesture.action) { + Settings.GestureSettings.GestureAction.OpenSearch -> R.string.gesture_action_open_search + Settings.GestureSettings.GestureAction.OpenNotificationDrawer -> R.string.gesture_action_notifications + Settings.GestureSettings.GestureAction.LockScreen -> R.string.gesture_action_lock_screen + Settings.GestureSettings.GestureAction.OpenQuickSettings -> R.string.gesture_action_quick_settings + Settings.GestureSettings.GestureAction.OpenRecents -> R.string.gesture_action_recents + Settings.GestureSettings.GestureAction.OpenPowerDialog -> R.string.gesture_action_power_menu + else -> R.string.gesture_action_none + }) + val gestureName = stringResource(when(failedGesture.gesture) { + Gesture.DoubleTap -> R.string.preference_gesture_double_tap + Gesture.LongPress -> R.string.preference_gesture_long_press + Gesture.SwipeDown -> R.string.preference_gesture_swipe_down + Gesture.SwipeLeft -> R.string.preference_gesture_swipe_left + Gesture.SwipeRight -> R.string.preference_gesture_swipe_right + }) + + BottomSheetDialog( + title = { Text(actionName) }, + onDismissRequest = onDismiss, + ) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(it) + ) { + Text( + stringResource(R.string.gesture_failed_message, gestureName, actionName), + style = MaterialTheme.typography.bodySmall + ) + val context = LocalLifecycleOwner.current + MissingPermissionBanner( + modifier = Modifier.padding(vertical = 16.dp), + text = stringResource(id = R.string.missing_permission_accessibility_gesture_failed), + secondaryAction = { + OutlinedButton(onClick = { + viewModel.disableGesture(failedGesture.gesture) + onDismiss() + }) { + Text(stringResource(R.string.turn_off)) + } + }, + onClick = { + viewModel.requestPermission(context as AppCompatActivity) + onDismiss() + }) + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/FailedGestureSheetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/FailedGestureSheetVM.kt new file mode 100644 index 00000000..4c82228d --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/FailedGestureSheetVM.kt @@ -0,0 +1,40 @@ +package de.mm20.launcher2.ui.launcher.sheets + +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import de.mm20.launcher2.permissions.PermissionGroup +import de.mm20.launcher2.permissions.PermissionsManager +import de.mm20.launcher2.preferences.LauncherDataStore +import de.mm20.launcher2.preferences.Settings.GestureSettings.GestureAction +import de.mm20.launcher2.ui.gestures.Gesture +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class FailedGestureSheetVM : ViewModel(), KoinComponent { + private val permissionsManager: PermissionsManager by inject() + private val dataStore: LauncherDataStore by inject() + + fun requestPermission(context: AppCompatActivity) { + permissionsManager.requestPermission(context, PermissionGroup.Accessibility) + } + + fun disableGesture(gesture: Gesture) { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder().setGestures( + it.gestures.toBuilder().apply { + when (gesture) { + Gesture.SwipeDown -> swipeDown = GestureAction.None + Gesture.SwipeLeft -> swipeLeft = GestureAction.None + Gesture.SwipeRight -> swipeRight = GestureAction.None + Gesture.DoubleTap -> doubleTap = GestureAction.None + Gesture.LongPress -> longPress = GestureAction.None + } + }.build() + ).build() + } + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt new file mode 100644 index 00000000..b11117c8 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt @@ -0,0 +1,16 @@ +package de.mm20.launcher2.ui.launcher.sheets + +import androidx.compose.runtime.Composable + +@Composable +fun LauncherBottomSheets() { + val bottomSheetManager = LocalBottomSheetManager.current + bottomSheetManager.customizeSearchableSheetShown.value?.let { + CustomizeSearchableSheet( + searchable = it, + onDismiss = { bottomSheetManager.dismissCustomizeSearchableModal() }) + } + if (bottomSheetManager.editFavoritesSheetShown.value) { + EditFavoritesSheet(onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() }) + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index e6cddad3..fd8f2b1b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -39,6 +39,7 @@ import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen import de.mm20.launcher2.ui.settings.easteregg.EasterEggSettingsScreen import de.mm20.launcher2.ui.settings.favorites.FavoritesSettingsScreen import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen +import de.mm20.launcher2.ui.settings.gestures.GestureSettingsScreen import de.mm20.launcher2.ui.settings.hiddenitems.HiddenItemsSettingsScreen import de.mm20.launcher2.ui.settings.layout.LayoutSettingsScreen import de.mm20.launcher2.ui.settings.license.LicenseScreen @@ -115,6 +116,9 @@ class SettingsActivity : BaseActivity() { composable("settings/search") { SearchSettingsScreen() } + composable("settings/gestures") { + GestureSettingsScreen() + } composable("settings/search/unitconverter") { UnitConverterSettingsScreen() } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt new file mode 100644 index 00000000..2f47bc5c --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt @@ -0,0 +1,131 @@ +package de.mm20.launcher2.ui.settings.gestures + +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.ktx.isAtLeastApiLevel +import de.mm20.launcher2.preferences.Settings.GestureSettings.GestureAction +import de.mm20.launcher2.preferences.Settings.LayoutSettings.Layout +import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.MissingPermissionBanner +import de.mm20.launcher2.ui.component.preferences.ListPreference +import de.mm20.launcher2.ui.component.preferences.PreferenceCategory +import de.mm20.launcher2.ui.component.preferences.PreferenceScreen + +@Composable +fun GestureSettingsScreen() { + val viewModel: GestureSettingsScreenVM = viewModel() + + val layout by viewModel.layout.observeAsState() + val hasPermission by viewModel.hasPermission.observeAsState() + + val options = buildList { + add(stringResource(R.string.gesture_action_none) to GestureAction.None) + add(stringResource(R.string.gesture_action_notifications) to GestureAction.OpenNotificationDrawer) + add(stringResource(R.string.gesture_action_quick_settings) to GestureAction.OpenQuickSettings) + if (isAtLeastApiLevel(28)) add(stringResource(R.string.gesture_action_lock_screen) to GestureAction.LockScreen) + add(stringResource(R.string.gesture_action_recents) to GestureAction.OpenRecents) + add(stringResource(R.string.gesture_action_power_menu) to GestureAction.OpenPowerDialog) + add(stringResource(R.string.gesture_action_open_search) to GestureAction.OpenSearch) + } + + val context = LocalContext.current + PreferenceScreen(title = stringResource(R.string.preference_screen_gestures)) { + item { + PreferenceCategory { + val doubleTap by viewModel.doubleTap.observeAsState() + AnimatedVisibility(hasPermission == false && requiresAccessibilityService(doubleTap)) { + MissingPermissionBanner( + modifier = Modifier.padding(16.dp), + text = stringResource(R.string.missing_permission_accessibility_gesture_settings), + onClick = { viewModel.requestPermission(context as AppCompatActivity) } + ) + } + ListPreference( + title = stringResource(R.string.preference_gesture_double_tap), + items = options, + value = doubleTap, + onValueChanged = { if (it != null) viewModel.setDoubleTap(it) } + ) + val longPress by viewModel.longPress.observeAsState() + AnimatedVisibility(hasPermission == false && requiresAccessibilityService(longPress)) { + MissingPermissionBanner( + modifier = Modifier.padding(16.dp), + text = stringResource(R.string.missing_permission_accessibility_gesture_settings), + onClick = { viewModel.requestPermission(context as AppCompatActivity) } + ) + } + ListPreference( + title = stringResource(R.string.preference_gesture_long_press), + items = options, + value = longPress, + onValueChanged = { if (it != null) viewModel.setLongPress(it) } + ) + val swipeDown by viewModel.swipeDown.observeAsState() + AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeDown)) { + MissingPermissionBanner( + modifier = Modifier.padding(16.dp), + text = stringResource(R.string.missing_permission_accessibility_gesture_settings), + onClick = { viewModel.requestPermission(context as AppCompatActivity) } + ) + } + ListPreference( + title = stringResource(R.string.preference_gesture_swipe_down), + enabled = layout != Layout.PullDown, + items = options, + value = if (layout == Layout.PullDown) GestureAction.OpenSearch else swipeDown, + onValueChanged = { if (it != null) viewModel.setSwipeDown(it) } + ) + val swipeLeft by viewModel.swipeLeft.observeAsState() + AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeLeft)) { + MissingPermissionBanner( + modifier = Modifier.padding(16.dp), + text = stringResource(R.string.missing_permission_accessibility_gesture_settings), + onClick = { viewModel.requestPermission(context as AppCompatActivity) } + ) + } + ListPreference( + title = stringResource(R.string.preference_gesture_swipe_left), + enabled = layout != Layout.Pager, + items = options, + value = if (layout == Layout.Pager) GestureAction.OpenSearch else swipeLeft, + onValueChanged = { if (it != null) viewModel.setSwipeLeft(it) } + ) + val swipeRight by viewModel.swipeRight.observeAsState() + AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeRight)) { + MissingPermissionBanner( + modifier = Modifier.padding(16.dp), + text = stringResource(R.string.missing_permission_accessibility_gesture_settings), + onClick = { viewModel.requestPermission(context as AppCompatActivity) } + ) + } + ListPreference( + title = stringResource(R.string.preference_gesture_swipe_right), + enabled = layout != Layout.PagerReversed, + items = options, + value = if (layout == Layout.PagerReversed) GestureAction.OpenSearch else swipeRight, + onValueChanged = { if (it != null) viewModel.setSwipeRight(it) } + ) + } + } + } +} + +fun requiresAccessibilityService(action: GestureAction?) : Boolean{ + return when(action) { + GestureAction.OpenNotificationDrawer, + GestureAction.LockScreen, + GestureAction.OpenQuickSettings, + GestureAction.OpenRecents, + GestureAction.OpenPowerDialog -> true + else -> false + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreenVM.kt new file mode 100644 index 00000000..b7f75010 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreenVM.kt @@ -0,0 +1,78 @@ +package de.mm20.launcher2.ui.settings.gestures + +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import de.mm20.launcher2.permissions.PermissionGroup +import de.mm20.launcher2.permissions.PermissionsManager +import de.mm20.launcher2.preferences.LauncherDataStore +import de.mm20.launcher2.preferences.Settings.GestureSettings.GestureAction +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class GestureSettingsScreenVM : ViewModel(), KoinComponent { + private val dataStore: LauncherDataStore by inject() + private val permissionsManager: PermissionsManager by inject() + + val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility).asLiveData() + + val layout = dataStore.data.map { it.layout.baseLayout }.asLiveData() + + val swipeDown = dataStore.data.map { it.gestures.swipeDown }.asLiveData() + val swipeLeft = dataStore.data.map { it.gestures.swipeLeft }.asLiveData() + val swipeRight = dataStore.data.map { it.gestures.swipeRight }.asLiveData() + val doubleTap = dataStore.data.map { it.gestures.doubleTap }.asLiveData() + val longPress = dataStore.data.map { it.gestures.longPress }.asLiveData() + + fun setSwipeDown(action: GestureAction) { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder().setGestures(it.gestures.toBuilder().setSwipeDown(action).build()) + .build() + } + } + } + + fun setSwipeLeft(action: GestureAction) { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder().setGestures(it.gestures.toBuilder().setSwipeLeft(action).build()) + .build() + } + } + } + + fun setSwipeRight(action: GestureAction) { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder().setGestures(it.gestures.toBuilder().setSwipeRight(action).build()) + .build() + } + } + } + + fun setDoubleTap(action: GestureAction) { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder().setGestures(it.gestures.toBuilder().setDoubleTap(action).build()) + .build() + } + } + } + + fun setLongPress(action: GestureAction) { + viewModelScope.launch { + dataStore.updateData { + it.toBuilder().setGestures(it.gestures.toBuilder().setLongPress(action).build()) + .build() + } + } + } + + fun requestPermission(context: AppCompatActivity) { + permissionsManager.requestPermission(context, PermissionGroup.Accessibility) + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/main/MainSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/main/MainSettingsScreen.kt index 5bae7e0a..106b1b05 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/main/MainSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/main/MainSettingsScreen.kt @@ -46,6 +46,14 @@ fun MainSettingsScreen() { navController?.navigate("settings/widgets") } ) + Preference( + icon = Icons.Rounded.Gesture, + title = stringResource(id = R.string.preference_screen_gestures), + summary = stringResource(id = R.string.preference_screen_gestures_summary), + onClick = { + navController?.navigate("settings/gestures") + } + ) Preference( icon = Icons.Rounded.NotificationBadge, title = stringResource(id = R.string.preference_screen_badges), diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 2cd49ecf..b1847020 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -732,4 +732,21 @@ Arrangement of search results Top-down Bottom-up + Gestures + Gestures + Swipe down + Swipe left + Swipe right + Double tap + Long press + None + Open search + Open notification drawer + Turn off screen + Open quick settings + Show power menu + Open recents + You have performed a \"%1$s\" gesture. This gesture is currently set to trigger a \"%2$s\" action. However, the action could not be performed for the following reason: + The launcher\'s accessibility service needs to be enabled to perform this action. + This action requires the launcher\'s accessibility service to be enabled. \ No newline at end of file diff --git a/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt b/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt index 7a599e05..e2cca2ba 100644 --- a/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt +++ b/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt @@ -47,6 +47,12 @@ interface PermissionsManager { * May not be called by anything else. */ fun reportNotificationListenerState(running: Boolean) + + /** + * Special function for the accessibility service to report its status. + * May not be called by anything else. + */ + fun reportAccessibilityServiceState(running: Boolean) } enum class PermissionGroup { @@ -56,6 +62,7 @@ enum class PermissionGroup { ExternalStorage, Notifications, AppShortcuts, + Accessibility, } internal class PermissionsManagerImpl( @@ -77,6 +84,7 @@ internal class PermissionsManagerImpl( checkPermissionOnce(PermissionGroup.Location) ) private val notificationsPermissionState = MutableStateFlow(false) + private val accessibilityPermissionState = MutableStateFlow(false) private val appShortcutsPermissionState = MutableStateFlow( checkPermissionOnce(PermissionGroup.AppShortcuts) ) @@ -90,6 +98,7 @@ internal class PermissionsManagerImpl( permissionGroup.ordinal ) } + PermissionGroup.Location -> { ActivityCompat.requestPermissions( context, @@ -97,6 +106,7 @@ internal class PermissionsManagerImpl( permissionGroup.ordinal ) } + PermissionGroup.Contacts -> { ActivityCompat.requestPermissions( context, @@ -104,6 +114,7 @@ internal class PermissionsManagerImpl( permissionGroup.ordinal ) } + PermissionGroup.ExternalStorage -> { if (isAtLeastApiLevel(Build.VERSION_CODES.R)) { val intent = @@ -120,6 +131,7 @@ internal class PermissionsManagerImpl( ) } } + PermissionGroup.Notifications -> { try { context.startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) @@ -127,10 +139,20 @@ internal class PermissionsManagerImpl( CrashReporter.logException(e) } } + PermissionGroup.AppShortcuts -> { context.tryStartActivity(Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)) pendingPermissionRequests.add(PermissionGroup.AppShortcuts) } + + PermissionGroup.Accessibility -> { + try { + context.tryStartActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) + pendingPermissionRequests.add(PermissionGroup.Accessibility) + } catch (e: ActivityNotFoundException) { + CrashReporter.logException(e) + } + } } } @@ -139,12 +161,15 @@ internal class PermissionsManagerImpl( PermissionGroup.Calendar -> { calendarPermissions.all { context.checkPermission(it) } } + PermissionGroup.Location -> { locationPermissions.all { context.checkPermission(it) } } + PermissionGroup.Contacts -> { contactPermissions.all { context.checkPermission(it) } } + PermissionGroup.ExternalStorage -> { if (isAtLeastApiLevel(Build.VERSION_CODES.R)) { Environment.isExternalStorageManager() @@ -152,12 +177,18 @@ internal class PermissionsManagerImpl( externalStoragePermissions.all { context.checkPermission(it) } } } + PermissionGroup.Notifications -> { notificationsPermissionState.value } + PermissionGroup.AppShortcuts -> { context.getSystemService()?.hasShortcutHostPermission() == true } + + PermissionGroup.Accessibility -> { + accessibilityPermissionState.value + } } } @@ -169,6 +200,7 @@ internal class PermissionsManagerImpl( PermissionGroup.ExternalStorage -> externalStoragePermissionState PermissionGroup.Notifications -> notificationsPermissionState PermissionGroup.AppShortcuts -> appShortcutsPermissionState + PermissionGroup.Accessibility -> accessibilityPermissionState } } @@ -186,6 +218,7 @@ internal class PermissionsManagerImpl( PermissionGroup.ExternalStorage -> externalStoragePermissionState.value = granted PermissionGroup.Notifications -> notificationsPermissionState.value = granted PermissionGroup.AppShortcuts -> appShortcutsPermissionState.value = granted + PermissionGroup.Accessibility -> accessibilityPermissionState.value = granted } } @@ -197,10 +230,12 @@ internal class PermissionsManagerImpl( externalStoragePermissionState.value = checkPermissionOnce(PermissionGroup.ExternalStorage) } + PermissionGroup.AppShortcuts -> { appShortcutsPermissionState.value = checkPermissionOnce(PermissionGroup.AppShortcuts) } + else -> {} } iterator.remove() @@ -211,6 +246,10 @@ internal class PermissionsManagerImpl( notificationsPermissionState.value = running } + override fun reportAccessibilityServiceState(running: Boolean) { + accessibilityPermissionState.value = running + } + companion object { private val calendarPermissions = arrayOf(Manifest.permission.READ_CALENDAR) private val locationPermissions = arrayOf( diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt index 9d5d473e..517b180e 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt @@ -1,7 +1,6 @@ package de.mm20.launcher2.preferences import android.content.Context -import android.graphics.Color import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors import scheme.Scheme @@ -14,11 +13,12 @@ fun createFactorySettings(context: Context): Settings { .setColorScheme(Settings.AppearanceSettings.ColorScheme.Default) .setDimWallpaper(false) .setBlurWallpaper(true) - .setCustomColors(Settings.AppearanceSettings.CustomColors.newBuilder() - .setAdvancedMode(false) - .setBaseColors(DefaultCustomColorsBase) - .setLightScheme(DefaultLightCustomColorScheme) - .setDarkScheme(DefaultDarkCustomColorScheme) + .setCustomColors( + Settings.AppearanceSettings.CustomColors.newBuilder() + .setAdvancedMode(false) + .setBaseColors(DefaultCustomColorsBase) + .setLightScheme(DefaultLightCustomColorScheme) + .setDarkScheme(DefaultDarkCustomColorScheme) ) .setFont(Settings.AppearanceSettings.Font.Outfit) .build() @@ -168,21 +168,29 @@ fun createFactorySettings(context: Context): Settings { .setBottomSearchBar(false) .setReverseSearchResults(false) ) + .setGestures( + Settings.GestureSettings.newBuilder() + .setDoubleTap(Settings.GestureSettings.GestureAction.LockScreen) + .setLongPress(Settings.GestureSettings.GestureAction.None) + .setSwipeDown(Settings.GestureSettings.GestureAction.OpenNotificationDrawer) + .setSwipeLeft(Settings.GestureSettings.GestureAction.None) + .setSwipeRight(Settings.GestureSettings.GestureAction.None) + ) .build() } internal val DefaultCustomColorsBase: Settings.AppearanceSettings.CustomColors.BaseColors -get() { - val scheme = Scheme.light(0xFFACE330.toInt()) - return Settings.AppearanceSettings.CustomColors.BaseColors.newBuilder() - .setAccent1(scheme.primary) - .setAccent2(scheme.secondary) - .setAccent3(scheme.tertiary) - .setNeutral1(scheme.surface) - .setNeutral2(scheme.surfaceVariant) - .setError(scheme.error) - .build() -} + get() { + val scheme = Scheme.light(0xFFACE330.toInt()) + return Settings.AppearanceSettings.CustomColors.BaseColors.newBuilder() + .setAccent1(scheme.primary) + .setAccent2(scheme.secondary) + .setAccent3(scheme.tertiary) + .setNeutral1(scheme.surface) + .setNeutral2(scheme.surfaceVariant) + .setError(scheme.error) + .build() + } internal val DefaultLightCustomColorScheme: Settings.AppearanceSettings.CustomColors.Scheme get() { diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/migrations/Migration_11_12.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/migrations/Migration_11_12.kt index 606441a1..92f9f35d 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/migrations/Migration_11_12.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/migrations/Migration_11_12.kt @@ -1,6 +1,7 @@ package de.mm20.launcher2.preferences.migrations import de.mm20.launcher2.preferences.Settings +import de.mm20.launcher2.preferences.Settings.GestureSettings import de.mm20.launcher2.preferences.Settings.LayoutSettings class Migration_11_12: VersionedMigration(11, 12) { @@ -30,6 +31,14 @@ class Migration_11_12: VersionedMigration(11, 12) { .setBottomSearchBar(false) .setReverseSearchResults(false) ) + .setGestures( + GestureSettings.newBuilder() + .setDoubleTap(GestureSettings.GestureAction.LockScreen) + .setLongPress(GestureSettings.GestureAction.None) + .setSwipeDown(GestureSettings.GestureAction.OpenNotificationDrawer) + .setSwipeLeft(GestureSettings.GestureAction.None) + .setSwipeRight(GestureSettings.GestureAction.None) + ) } } return builder diff --git a/core/preferences/src/main/proto/settings.proto b/core/preferences/src/main/proto/settings.proto index 599024f2..2bbf2f28 100644 --- a/core/preferences/src/main/proto/settings.proto +++ b/core/preferences/src/main/proto/settings.proto @@ -299,4 +299,22 @@ message Settings { bool reverse_search_results = 3; } LayoutSettings layout = 27; + + message GestureSettings { + enum GestureAction { + None = 0; + OpenSearch = 1; + OpenNotificationDrawer = 2; + LockScreen = 3; + OpenQuickSettings = 4; + OpenRecents = 5; + OpenPowerDialog = 6; + } + GestureAction swipe_down = 1; + GestureAction swipe_left = 2; + GestureAction swipe_right = 3; + GestureAction double_tap = 4; + GestureAction long_press = 5; + } + GestureSettings gestures = 28; } \ No newline at end of file diff --git a/services/global-actions/.gitignore b/services/global-actions/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/services/global-actions/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/services/global-actions/build.gradle.kts b/services/global-actions/build.gradle.kts new file mode 100644 index 00000000..6d757791 --- /dev/null +++ b/services/global-actions/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + id("com.android.library") + id("kotlin-android") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + namespace = "de.mm20.launcher2.globalactions" +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.koin.android) + + implementation(project(":core:preferences")) + implementation(project(":core:base")) + implementation(project(":core:i18n")) + implementation(project(":core:permissions")) + +} \ No newline at end of file diff --git a/services/global-actions/consumer-rules.pro b/services/global-actions/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/services/global-actions/proguard-rules.pro b/services/global-actions/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/services/global-actions/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/services/global-actions/src/main/AndroidManifest.xml b/services/global-actions/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d723dce9 --- /dev/null +++ b/services/global-actions/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/services/global-actions/src/main/java/de/mm20/launcher2/globalactions/GlobalActionsService.kt b/services/global-actions/src/main/java/de/mm20/launcher2/globalactions/GlobalActionsService.kt new file mode 100644 index 00000000..0a1b0442 --- /dev/null +++ b/services/global-actions/src/main/java/de/mm20/launcher2/globalactions/GlobalActionsService.kt @@ -0,0 +1,25 @@ +package de.mm20.launcher2.globalactions + +import android.accessibilityservice.AccessibilityService + +class GlobalActionsService { + fun openNotificationDrawer() { + LauncherAccessibilityService.getInstance()?.performGlobalAction(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS) + } + + fun lockScreen() { + LauncherAccessibilityService.getInstance()?.performGlobalAction(AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN) + } + + fun openQuickSettings() { + LauncherAccessibilityService.getInstance()?.performGlobalAction(AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS) + } + + fun openPowerDialog() { + LauncherAccessibilityService.getInstance()?.performGlobalAction(AccessibilityService.GLOBAL_ACTION_POWER_DIALOG) + } + + fun openRecents() { + LauncherAccessibilityService.getInstance()?.performGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS) + } +} \ No newline at end of file diff --git a/services/global-actions/src/main/java/de/mm20/launcher2/globalactions/LauncherAccessibilityService.kt b/services/global-actions/src/main/java/de/mm20/launcher2/globalactions/LauncherAccessibilityService.kt new file mode 100644 index 00000000..0e45ec95 --- /dev/null +++ b/services/global-actions/src/main/java/de/mm20/launcher2/globalactions/LauncherAccessibilityService.kt @@ -0,0 +1,40 @@ +package de.mm20.launcher2.globalactions + +import android.accessibilityservice.AccessibilityService +import android.content.Intent +import android.view.accessibility.AccessibilityEvent +import de.mm20.launcher2.permissions.PermissionsManager +import org.koin.android.ext.android.inject +import java.lang.ref.WeakReference + +class LauncherAccessibilityService: AccessibilityService() { + + private val permissionManager: PermissionsManager by inject() + + override fun onAccessibilityEvent(event: AccessibilityEvent?) { + + } + + override fun onInterrupt() { + + } + + override fun onServiceConnected() { + super.onServiceConnected() + instance = WeakReference(this) + permissionManager.reportAccessibilityServiceState(true) + } + + override fun onUnbind(intent: Intent?): Boolean { + permissionManager.reportAccessibilityServiceState(false) + instance = null + return super.onUnbind(intent) + } + + companion object { + private var instance: WeakReference? = null + internal fun getInstance(): LauncherAccessibilityService? { + return instance?.get() + } + } +} \ No newline at end of file diff --git a/services/global-actions/src/main/java/de/mm20/launcher2/globalactions/Module.kt b/services/global-actions/src/main/java/de/mm20/launcher2/globalactions/Module.kt new file mode 100644 index 00000000..4219d061 --- /dev/null +++ b/services/global-actions/src/main/java/de/mm20/launcher2/globalactions/Module.kt @@ -0,0 +1,7 @@ +package de.mm20.launcher2.globalactions + +import org.koin.dsl.module + +val globalActionsModule = module { + single { GlobalActionsService() } +} \ No newline at end of file diff --git a/services/global-actions/src/main/res/xml/accessibility_service.xml b/services/global-actions/src/main/res/xml/accessibility_service.xml new file mode 100644 index 00000000..f4c39bc9 --- /dev/null +++ b/services/global-actions/src/main/res/xml/accessibility_service.xml @@ -0,0 +1,5 @@ + + diff --git a/settings.gradle.kts b/settings.gradle.kts index afde6235..3821d9a5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -294,3 +294,4 @@ include(":libs:owncloud") include(":libs:webdav") include(":libs:g-services") include(":libs:ms-services") +include(":services:global-actions")