Gestures
This commit is contained in:
parent
4fba96a75a
commit
c12c07fa45
@ -137,6 +137,7 @@ dependencies {
|
|||||||
implementation(project(":data:wikipedia"))
|
implementation(project(":data:wikipedia"))
|
||||||
implementation(project(":core:database"))
|
implementation(project(":core:database"))
|
||||||
implementation(project(":data:search-actions"))
|
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
|
// Uncomment this if you want annoying notifications in your debug builds yelling at you how terrible your code is
|
||||||
//debugImplementation(libs.leakcanary)
|
//debugImplementation(libs.leakcanary)
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import de.mm20.launcher2.websites.websitesModule
|
|||||||
import de.mm20.launcher2.widgets.widgetsModule
|
import de.mm20.launcher2.widgets.widgetsModule
|
||||||
import de.mm20.launcher2.wikipedia.wikipediaModule
|
import de.mm20.launcher2.wikipedia.wikipediaModule
|
||||||
import de.mm20.launcher2.database.databaseModule
|
import de.mm20.launcher2.database.databaseModule
|
||||||
|
import de.mm20.launcher2.globalactions.globalActionsModule
|
||||||
import de.mm20.launcher2.notifications.notificationsModule
|
import de.mm20.launcher2.notifications.notificationsModule
|
||||||
import de.mm20.launcher2.permissions.permissionsModule
|
import de.mm20.launcher2.permissions.permissionsModule
|
||||||
import de.mm20.launcher2.preferences.preferencesModule
|
import de.mm20.launcher2.preferences.preferencesModule
|
||||||
@ -64,6 +65,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
|
|||||||
databaseModule,
|
databaseModule,
|
||||||
favoritesModule,
|
favoritesModule,
|
||||||
filesModule,
|
filesModule,
|
||||||
|
globalActionsModule,
|
||||||
iconsModule,
|
iconsModule,
|
||||||
musicModule,
|
musicModule,
|
||||||
notificationsModule,
|
notificationsModule,
|
||||||
|
|||||||
@ -143,4 +143,5 @@ dependencies {
|
|||||||
implementation(project(":services:accounts"))
|
implementation(project(":services:accounts"))
|
||||||
implementation(project(":services:backup"))
|
implementation(project(":services:backup"))
|
||||||
implementation(project(":data:search-actions"))
|
implementation(project(":data:search-actions"))
|
||||||
|
implementation(project(":services:global-actions"))
|
||||||
}
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package de.mm20.launcher2.ui.gestures
|
||||||
|
|
||||||
|
enum class Gesture {
|
||||||
|
DoubleTap,
|
||||||
|
LongPress,
|
||||||
|
SwipeDown,
|
||||||
|
SwipeLeft,
|
||||||
|
SwipeRight,
|
||||||
|
}
|
||||||
@ -1,12 +1,23 @@
|
|||||||
package de.mm20.launcher2.ui.launcher
|
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.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
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.LauncherDataStore
|
||||||
import de.mm20.launcher2.preferences.Settings
|
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.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@ -17,6 +28,19 @@ import org.koin.core.component.inject
|
|||||||
class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
||||||
|
|
||||||
private val dataStore: LauncherDataStore by inject()
|
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)
|
private var isSystemInDarkMode = MutableStateFlow(false)
|
||||||
|
|
||||||
@ -84,4 +108,68 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
|||||||
val fillClockHeight = dataStore.data.map { it.clockWidget.fillHeight }.asLiveData()
|
val fillClockHeight = dataStore.data.map { it.clockWidget.fillHeight }.asLiveData()
|
||||||
val searchBarColor = dataStore.data.map { it.searchBar.color }.asLiveData()
|
val searchBarColor = dataStore.data.map { it.searchBar.color }.asLiveData()
|
||||||
val searchBarStyle = dataStore.data.map { it.searchBar.searchBarStyle }.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)
|
||||||
@ -4,7 +4,6 @@ import android.app.WallpaperManager
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@ -37,6 +36,7 @@ import androidx.core.view.WindowInsetsControllerCompat
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.flowWithLifecycle
|
import androidx.lifecycle.flowWithLifecycle
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
|
import de.mm20.launcher2.globalactions.GlobalActionsService
|
||||||
import de.mm20.launcher2.preferences.Settings
|
import de.mm20.launcher2.preferences.Settings
|
||||||
import de.mm20.launcher2.preferences.Settings.SystemBarsSettings.SystemBarColors
|
import de.mm20.launcher2.preferences.Settings.SystemBarsSettings.SystemBarColors
|
||||||
import de.mm20.launcher2.ui.assistant.AssistantScaffold
|
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.ProvideCurrentTime
|
||||||
import de.mm20.launcher2.ui.base.ProvideSettings
|
import de.mm20.launcher2.ui.base.ProvideSettings
|
||||||
import de.mm20.launcher2.ui.component.NavBarEffects
|
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.GestureDetector
|
||||||
import de.mm20.launcher2.ui.gestures.GestureHandler
|
import de.mm20.launcher2.ui.gestures.GestureHandler
|
||||||
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
|
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
|
||||||
import de.mm20.launcher2.ui.ktx.animateTo
|
import de.mm20.launcher2.ui.ktx.animateTo
|
||||||
import de.mm20.launcher2.ui.ktx.toPixels
|
import de.mm20.launcher2.ui.ktx.toPixels
|
||||||
import de.mm20.launcher2.ui.launcher.sheets.CustomizeSearchableSheet
|
import de.mm20.launcher2.ui.launcher.sheets.FailedGestureSheet
|
||||||
import de.mm20.launcher2.ui.launcher.sheets.EditFavoritesSheet
|
import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheets
|
||||||
import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheetManager
|
import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheetManager
|
||||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||||
import de.mm20.launcher2.ui.launcher.transitions.HomeTransition
|
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.locals.LocalWindowSize
|
||||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
import de.mm20.launcher2.ui.theme.LauncherTheme
|
||||||
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
|
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
|
||||||
@ -70,7 +72,9 @@ abstract class SharedLauncherActivity(
|
|||||||
private val mode: LauncherActivityMode
|
private val mode: LauncherActivityMode
|
||||||
) : BaseActivity() {
|
) : BaseActivity() {
|
||||||
|
|
||||||
private val viewModel: LauncherActivityVM by viewModels()
|
private val viewModel: LauncherScaffoldVM by viewModels()
|
||||||
|
|
||||||
|
private val globalActionsService: GlobalActionsService by inject()
|
||||||
|
|
||||||
internal val homeTransitionManager = HomeTransitionManager()
|
internal val homeTransitionManager = HomeTransitionManager()
|
||||||
|
|
||||||
@ -256,52 +260,43 @@ abstract class SharedLauncherActivity(
|
|||||||
) { enterTransitionProgress.value }
|
) { enterTransitionProgress.value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LauncherBottomSheets()
|
||||||
bottomSheetManager.customizeSearchableSheetShown.value?.let {
|
|
||||||
CustomizeSearchableSheet(
|
|
||||||
searchable = it,
|
|
||||||
onDismiss = { bottomSheetManager.dismissCustomizeSearchableModal() })
|
|
||||||
}
|
|
||||||
if (bottomSheetManager.editFavoritesSheetShown.value) {
|
|
||||||
EditFavoritesSheet(onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val swipeThreshold = 150.dp.toPixels()
|
val swipeThreshold = 150.dp.toPixels()
|
||||||
GestureHandler(
|
GestureHandler(
|
||||||
detector = gestureDetector,
|
detector = gestureDetector,
|
||||||
onDoubleTap = {
|
onDoubleTap = {
|
||||||
Log.d("MM20", "Double tap")
|
viewModel.handleGesture(Gesture.DoubleTap)
|
||||||
},
|
},
|
||||||
onLongPress = {
|
onLongPress = {
|
||||||
Log.d("MM20", "Long press")
|
viewModel.handleGesture(Gesture.LongPress)
|
||||||
},
|
},
|
||||||
onDrag = {
|
onDrag = {
|
||||||
return@GestureHandler when {
|
return@GestureHandler when {
|
||||||
it.x > swipeThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> {
|
it.x > swipeThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> {
|
||||||
Log.d("MM20", "Swipe right")
|
viewModel.handleGesture(Gesture.SwipeRight)
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it.x < -swipeThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> {
|
it.x < -swipeThreshold && it.x.absoluteValue > it.y.absoluteValue * 2f -> {
|
||||||
Log.d("MM20", "Swipe left")
|
viewModel.handleGesture(Gesture.SwipeLeft)
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it.y > swipeThreshold && it.y.absoluteValue > it.x.absoluteValue * 2f -> {
|
it.y > swipeThreshold && it.y.absoluteValue > it.x.absoluteValue * 2f -> {
|
||||||
Log.d("MM20", "Swipe down")
|
viewModel.handleGesture(Gesture.SwipeDown)
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it.y < -swipeThreshold && it.y.absoluteValue > it.x.absoluteValue * 2f -> {
|
|
||||||
Log.d("MM20", "Swipe up")
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if (viewModel.failedGestureState != null) {
|
||||||
|
FailedGestureSheet(
|
||||||
|
failedGesture = viewModel.failedGestureState!!,
|
||||||
|
onDismiss = {
|
||||||
|
viewModel.dismissGestureFailedSheet()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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() })
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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.easteregg.EasterEggSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.favorites.FavoritesSettingsScreen
|
import de.mm20.launcher2.ui.settings.favorites.FavoritesSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
|
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.hiddenitems.HiddenItemsSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.layout.LayoutSettingsScreen
|
import de.mm20.launcher2.ui.settings.layout.LayoutSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.license.LicenseScreen
|
import de.mm20.launcher2.ui.settings.license.LicenseScreen
|
||||||
@ -115,6 +116,9 @@ class SettingsActivity : BaseActivity() {
|
|||||||
composable("settings/search") {
|
composable("settings/search") {
|
||||||
SearchSettingsScreen()
|
SearchSettingsScreen()
|
||||||
}
|
}
|
||||||
|
composable("settings/gestures") {
|
||||||
|
GestureSettingsScreen()
|
||||||
|
}
|
||||||
composable("settings/search/unitconverter") {
|
composable("settings/search/unitconverter") {
|
||||||
UnitConverterSettingsScreen()
|
UnitConverterSettingsScreen()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -46,6 +46,14 @@ fun MainSettingsScreen() {
|
|||||||
navController?.navigate("settings/widgets")
|
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(
|
Preference(
|
||||||
icon = Icons.Rounded.NotificationBadge,
|
icon = Icons.Rounded.NotificationBadge,
|
||||||
title = stringResource(id = R.string.preference_screen_badges),
|
title = stringResource(id = R.string.preference_screen_badges),
|
||||||
|
|||||||
@ -732,4 +732,21 @@
|
|||||||
<string name="preference_layout_search_results">Arrangement of search results</string>
|
<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_top_down">Top-down</string>
|
||||||
<string name="search_results_order_bottom_up">Bottom-up</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>
|
</resources>
|
||||||
@ -47,6 +47,12 @@ interface PermissionsManager {
|
|||||||
* May not be called by anything else.
|
* May not be called by anything else.
|
||||||
*/
|
*/
|
||||||
fun reportNotificationListenerState(running: Boolean)
|
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 {
|
enum class PermissionGroup {
|
||||||
@ -56,6 +62,7 @@ enum class PermissionGroup {
|
|||||||
ExternalStorage,
|
ExternalStorage,
|
||||||
Notifications,
|
Notifications,
|
||||||
AppShortcuts,
|
AppShortcuts,
|
||||||
|
Accessibility,
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class PermissionsManagerImpl(
|
internal class PermissionsManagerImpl(
|
||||||
@ -77,6 +84,7 @@ internal class PermissionsManagerImpl(
|
|||||||
checkPermissionOnce(PermissionGroup.Location)
|
checkPermissionOnce(PermissionGroup.Location)
|
||||||
)
|
)
|
||||||
private val notificationsPermissionState = MutableStateFlow(false)
|
private val notificationsPermissionState = MutableStateFlow(false)
|
||||||
|
private val accessibilityPermissionState = MutableStateFlow(false)
|
||||||
private val appShortcutsPermissionState = MutableStateFlow(
|
private val appShortcutsPermissionState = MutableStateFlow(
|
||||||
checkPermissionOnce(PermissionGroup.AppShortcuts)
|
checkPermissionOnce(PermissionGroup.AppShortcuts)
|
||||||
)
|
)
|
||||||
@ -90,6 +98,7 @@ internal class PermissionsManagerImpl(
|
|||||||
permissionGroup.ordinal
|
permissionGroup.ordinal
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionGroup.Location -> {
|
PermissionGroup.Location -> {
|
||||||
ActivityCompat.requestPermissions(
|
ActivityCompat.requestPermissions(
|
||||||
context,
|
context,
|
||||||
@ -97,6 +106,7 @@ internal class PermissionsManagerImpl(
|
|||||||
permissionGroup.ordinal
|
permissionGroup.ordinal
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionGroup.Contacts -> {
|
PermissionGroup.Contacts -> {
|
||||||
ActivityCompat.requestPermissions(
|
ActivityCompat.requestPermissions(
|
||||||
context,
|
context,
|
||||||
@ -104,6 +114,7 @@ internal class PermissionsManagerImpl(
|
|||||||
permissionGroup.ordinal
|
permissionGroup.ordinal
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionGroup.ExternalStorage -> {
|
PermissionGroup.ExternalStorage -> {
|
||||||
if (isAtLeastApiLevel(Build.VERSION_CODES.R)) {
|
if (isAtLeastApiLevel(Build.VERSION_CODES.R)) {
|
||||||
val intent =
|
val intent =
|
||||||
@ -120,6 +131,7 @@ internal class PermissionsManagerImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionGroup.Notifications -> {
|
PermissionGroup.Notifications -> {
|
||||||
try {
|
try {
|
||||||
context.startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
|
context.startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
|
||||||
@ -127,10 +139,20 @@ internal class PermissionsManagerImpl(
|
|||||||
CrashReporter.logException(e)
|
CrashReporter.logException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionGroup.AppShortcuts -> {
|
PermissionGroup.AppShortcuts -> {
|
||||||
context.tryStartActivity(Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS))
|
context.tryStartActivity(Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS))
|
||||||
pendingPermissionRequests.add(PermissionGroup.AppShortcuts)
|
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 -> {
|
PermissionGroup.Calendar -> {
|
||||||
calendarPermissions.all { context.checkPermission(it) }
|
calendarPermissions.all { context.checkPermission(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionGroup.Location -> {
|
PermissionGroup.Location -> {
|
||||||
locationPermissions.all { context.checkPermission(it) }
|
locationPermissions.all { context.checkPermission(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionGroup.Contacts -> {
|
PermissionGroup.Contacts -> {
|
||||||
contactPermissions.all { context.checkPermission(it) }
|
contactPermissions.all { context.checkPermission(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionGroup.ExternalStorage -> {
|
PermissionGroup.ExternalStorage -> {
|
||||||
if (isAtLeastApiLevel(Build.VERSION_CODES.R)) {
|
if (isAtLeastApiLevel(Build.VERSION_CODES.R)) {
|
||||||
Environment.isExternalStorageManager()
|
Environment.isExternalStorageManager()
|
||||||
@ -152,12 +177,18 @@ internal class PermissionsManagerImpl(
|
|||||||
externalStoragePermissions.all { context.checkPermission(it) }
|
externalStoragePermissions.all { context.checkPermission(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionGroup.Notifications -> {
|
PermissionGroup.Notifications -> {
|
||||||
notificationsPermissionState.value
|
notificationsPermissionState.value
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionGroup.AppShortcuts -> {
|
PermissionGroup.AppShortcuts -> {
|
||||||
context.getSystemService<LauncherApps>()?.hasShortcutHostPermission() == true
|
context.getSystemService<LauncherApps>()?.hasShortcutHostPermission() == true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PermissionGroup.Accessibility -> {
|
||||||
|
accessibilityPermissionState.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,6 +200,7 @@ internal class PermissionsManagerImpl(
|
|||||||
PermissionGroup.ExternalStorage -> externalStoragePermissionState
|
PermissionGroup.ExternalStorage -> externalStoragePermissionState
|
||||||
PermissionGroup.Notifications -> notificationsPermissionState
|
PermissionGroup.Notifications -> notificationsPermissionState
|
||||||
PermissionGroup.AppShortcuts -> appShortcutsPermissionState
|
PermissionGroup.AppShortcuts -> appShortcutsPermissionState
|
||||||
|
PermissionGroup.Accessibility -> accessibilityPermissionState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,6 +218,7 @@ internal class PermissionsManagerImpl(
|
|||||||
PermissionGroup.ExternalStorage -> externalStoragePermissionState.value = granted
|
PermissionGroup.ExternalStorage -> externalStoragePermissionState.value = granted
|
||||||
PermissionGroup.Notifications -> notificationsPermissionState.value = granted
|
PermissionGroup.Notifications -> notificationsPermissionState.value = granted
|
||||||
PermissionGroup.AppShortcuts -> appShortcutsPermissionState.value = granted
|
PermissionGroup.AppShortcuts -> appShortcutsPermissionState.value = granted
|
||||||
|
PermissionGroup.Accessibility -> accessibilityPermissionState.value = granted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,10 +230,12 @@ internal class PermissionsManagerImpl(
|
|||||||
externalStoragePermissionState.value =
|
externalStoragePermissionState.value =
|
||||||
checkPermissionOnce(PermissionGroup.ExternalStorage)
|
checkPermissionOnce(PermissionGroup.ExternalStorage)
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionGroup.AppShortcuts -> {
|
PermissionGroup.AppShortcuts -> {
|
||||||
appShortcutsPermissionState.value =
|
appShortcutsPermissionState.value =
|
||||||
checkPermissionOnce(PermissionGroup.AppShortcuts)
|
checkPermissionOnce(PermissionGroup.AppShortcuts)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
@ -211,6 +246,10 @@ internal class PermissionsManagerImpl(
|
|||||||
notificationsPermissionState.value = running
|
notificationsPermissionState.value = running
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun reportAccessibilityServiceState(running: Boolean) {
|
||||||
|
accessibilityPermissionState.value = running
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val calendarPermissions = arrayOf(Manifest.permission.READ_CALENDAR)
|
private val calendarPermissions = arrayOf(Manifest.permission.READ_CALENDAR)
|
||||||
private val locationPermissions = arrayOf(
|
private val locationPermissions = arrayOf(
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package de.mm20.launcher2.preferences
|
package de.mm20.launcher2.preferences
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
|
||||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
|
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
|
||||||
import scheme.Scheme
|
import scheme.Scheme
|
||||||
|
|
||||||
@ -14,7 +13,8 @@ fun createFactorySettings(context: Context): Settings {
|
|||||||
.setColorScheme(Settings.AppearanceSettings.ColorScheme.Default)
|
.setColorScheme(Settings.AppearanceSettings.ColorScheme.Default)
|
||||||
.setDimWallpaper(false)
|
.setDimWallpaper(false)
|
||||||
.setBlurWallpaper(true)
|
.setBlurWallpaper(true)
|
||||||
.setCustomColors(Settings.AppearanceSettings.CustomColors.newBuilder()
|
.setCustomColors(
|
||||||
|
Settings.AppearanceSettings.CustomColors.newBuilder()
|
||||||
.setAdvancedMode(false)
|
.setAdvancedMode(false)
|
||||||
.setBaseColors(DefaultCustomColorsBase)
|
.setBaseColors(DefaultCustomColorsBase)
|
||||||
.setLightScheme(DefaultLightCustomColorScheme)
|
.setLightScheme(DefaultLightCustomColorScheme)
|
||||||
@ -168,11 +168,19 @@ fun createFactorySettings(context: Context): Settings {
|
|||||||
.setBottomSearchBar(false)
|
.setBottomSearchBar(false)
|
||||||
.setReverseSearchResults(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()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val DefaultCustomColorsBase: Settings.AppearanceSettings.CustomColors.BaseColors
|
internal val DefaultCustomColorsBase: Settings.AppearanceSettings.CustomColors.BaseColors
|
||||||
get() {
|
get() {
|
||||||
val scheme = Scheme.light(0xFFACE330.toInt())
|
val scheme = Scheme.light(0xFFACE330.toInt())
|
||||||
return Settings.AppearanceSettings.CustomColors.BaseColors.newBuilder()
|
return Settings.AppearanceSettings.CustomColors.BaseColors.newBuilder()
|
||||||
.setAccent1(scheme.primary)
|
.setAccent1(scheme.primary)
|
||||||
@ -182,7 +190,7 @@ get() {
|
|||||||
.setNeutral2(scheme.surfaceVariant)
|
.setNeutral2(scheme.surfaceVariant)
|
||||||
.setError(scheme.error)
|
.setError(scheme.error)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val DefaultLightCustomColorScheme: Settings.AppearanceSettings.CustomColors.Scheme
|
internal val DefaultLightCustomColorScheme: Settings.AppearanceSettings.CustomColors.Scheme
|
||||||
get() {
|
get() {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package de.mm20.launcher2.preferences.migrations
|
package de.mm20.launcher2.preferences.migrations
|
||||||
|
|
||||||
import de.mm20.launcher2.preferences.Settings
|
import de.mm20.launcher2.preferences.Settings
|
||||||
|
import de.mm20.launcher2.preferences.Settings.GestureSettings
|
||||||
import de.mm20.launcher2.preferences.Settings.LayoutSettings
|
import de.mm20.launcher2.preferences.Settings.LayoutSettings
|
||||||
|
|
||||||
class Migration_11_12: VersionedMigration(11, 12) {
|
class Migration_11_12: VersionedMigration(11, 12) {
|
||||||
@ -30,6 +31,14 @@ class Migration_11_12: VersionedMigration(11, 12) {
|
|||||||
.setBottomSearchBar(false)
|
.setBottomSearchBar(false)
|
||||||
.setReverseSearchResults(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
|
return builder
|
||||||
|
|||||||
@ -299,4 +299,22 @@ message Settings {
|
|||||||
bool reverse_search_results = 3;
|
bool reverse_search_results = 3;
|
||||||
}
|
}
|
||||||
LayoutSettings layout = 27;
|
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
1
services/global-actions/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
47
services/global-actions/build.gradle.kts
Normal file
47
services/global-actions/build.gradle.kts
Normal 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"))
|
||||||
|
|
||||||
|
}
|
||||||
0
services/global-actions/consumer-rules.pro
Normal file
0
services/global-actions/consumer-rules.pro
Normal file
21
services/global-actions/proguard-rules.pro
vendored
Normal file
21
services/global-actions/proguard-rules.pro
vendored
Normal 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
|
||||||
17
services/global-actions/src/main/AndroidManifest.xml
Normal file
17
services/global-actions/src/main/AndroidManifest.xml
Normal 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>
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package de.mm20.launcher2.globalactions
|
||||||
|
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val globalActionsModule = module {
|
||||||
|
single { GlobalActionsService() }
|
||||||
|
}
|
||||||
@ -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" />
|
||||||
@ -294,3 +294,4 @@ include(":libs:owncloud")
|
|||||||
include(":libs:webdav")
|
include(":libs:webdav")
|
||||||
include(":libs:g-services")
|
include(":libs:g-services")
|
||||||
include(":libs:ms-services")
|
include(":libs:ms-services")
|
||||||
|
include(":services:global-actions")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user