This commit is contained in:
MM20 2023-01-17 18:55:30 +01:00
parent 4fba96a75a
commit c12c07fa45
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
28 changed files with 752 additions and 45 deletions

View File

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

View File

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

View File

@ -143,4 +143,5 @@ dependencies {
implementation(project(":services:accounts"))
implementation(project(":services:backup"))
implementation(project(":data:search-actions"))
implementation(project(":services:global-actions"))
}

View File

@ -0,0 +1,9 @@
package de.mm20.launcher2.ui.gestures
enum class Gesture {
DoubleTap,
LongPress,
SwipeDown,
SwipeLeft,
SwipeRight,
}

View File

@ -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()
}
var failedGestureState by mutableStateOf<FailedGesture?>(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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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),

View File

@ -732,4 +732,21 @@
<string name="preference_layout_search_results">Arrangement of search results</string>
<string name="search_results_order_top_down">Top-down</string>
<string name="search_results_order_bottom_up">Bottom-up</string>
<string name="preference_screen_gestures">Gestures</string>
<string name="preference_screen_gestures_summary">Gestures</string>
<string name="preference_gesture_swipe_down">Swipe down</string>
<string name="preference_gesture_swipe_left">Swipe left</string>
<string name="preference_gesture_swipe_right">Swipe right</string>
<string name="preference_gesture_double_tap">Double tap</string>
<string name="preference_gesture_long_press">Long press</string>
<string name="gesture_action_none">None</string>
<string name="gesture_action_open_search">Open search</string>
<string name="gesture_action_notifications">Open notification drawer</string>
<string name="gesture_action_lock_screen">Turn off screen</string>
<string name="gesture_action_quick_settings">Open quick settings</string>
<string name="gesture_action_power_menu">Show power menu</string>
<string name="gesture_action_recents">Open recents</string>
<string name="gesture_failed_message">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:</string>
<string name="missing_permission_accessibility_gesture_failed">The launcher\'s accessibility service needs to be enabled to perform this action.</string>
<string name="missing_permission_accessibility_gesture_settings">This action requires the launcher\'s accessibility service to be enabled.</string>
</resources>

View File

@ -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<LauncherApps>()?.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(

View File

@ -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() {

View File

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

View File

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

1
services/global-actions/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

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

View File

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

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service
android:name=".LauncherAccessibilityService"
android:exported="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service" />
</service>
</application>
</manifest>

View File

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

View File

@ -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<LauncherAccessibilityService>? = null
internal fun getInstance(): LauncherAccessibilityService? {
return instance?.get()
}
}
}

View File

@ -0,0 +1,7 @@
package de.mm20.launcher2.globalactions
import org.koin.dsl.module
val globalActionsModule = module {
single { GlobalActionsService() }
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes=""
android:canRetrieveWindowContent="false"
android:isAccessibilityTool="false" />

View File

@ -294,3 +294,4 @@ include(":libs:owncloud")
include(":libs:webdav")
include(":libs:g-services")
include(":libs:ms-services")
include(":services:global-actions")