diff --git a/build.gradle b/build.gradle
index f221720ee4..c6cebfca73 100644
--- a/build.gradle
+++ b/build.gradle
@@ -15,6 +15,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:7.2.0'
classpath PROTOBUF_CLASS_PATH
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
}
}
@@ -88,6 +89,7 @@ apply plugin: 'com.google.protobuf'
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
+apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
final def commitHash = { ->
final def stdout = new ByteArrayOutputStream()
@@ -337,6 +339,7 @@ dependencies {
withoutQuickstepImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/libs", include: 'plugin_core.jar')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'org.mockito:mockito-core:4.5.1'
androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml
index f9270c7885..f17865f0f1 100644
--- a/lawnchair/res/values/strings.xml
+++ b/lawnchair/res/values/strings.xml
@@ -288,4 +288,8 @@
Invalid backup file.
Google
"%1$d%% — Full in %2$s"
+ Double-Tap
+ Do Nothing
+ Sleep
+ Open Notification Panel
diff --git a/lawnchair/src/app/lawnchair/gestures/GestureController.kt b/lawnchair/src/app/lawnchair/gestures/GestureController.kt
index 09adb6aff6..f80520a890 100644
--- a/lawnchair/src/app/lawnchair/gestures/GestureController.kt
+++ b/lawnchair/src/app/lawnchair/gestures/GestureController.kt
@@ -18,31 +18,31 @@ package app.lawnchair.gestures
import androidx.lifecycle.lifecycleScope
import app.lawnchair.LawnchairLauncher
-import app.lawnchair.gestures.handlers.SleepGestureHandler
+import app.lawnchair.gestures.config.GestureHandlerConfig
import app.lawnchair.preferences2.PreferenceManager2
-import com.patrykmichalik.preferencemanager.onEach
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
+import com.patrykmichalik.preferencemanager.Preference
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class GestureController(private val launcher: LawnchairLauncher) {
- private val preferenceManager = PreferenceManager2.getInstance(launcher)
- private val doubleTapHandler = SleepGestureHandler(launcher)
- private val coroutineScope = CoroutineScope(Dispatchers.IO)
- private var dt2s = false
+ private val prefs = PreferenceManager2.getInstance(launcher)
+ private val scope = MainScope()
- init {
- preferenceManager.dt2s.onEach(launchIn = coroutineScope) {
- dt2s = it
- }
- }
+ private val doubleTapHandler = handler(prefs.doubleTapHandler)
fun onDoubleTap() {
- // TODO: proper gesture selection system
- if (dt2s) {
- launcher.lifecycleScope.launch {
- doubleTapHandler.onTrigger(launcher)
- }
+ launcher.lifecycleScope.launch {
+ doubleTapHandler.first().onTrigger(launcher)
}
}
+
+ private fun handler(pref: Preference) = pref.get()
+ .distinctUntilChanged()
+ .map { it.createHandler(launcher) }
+ .shareIn(
+ scope,
+ SharingStarted.Lazily,
+ replay = 1
+ )
}
diff --git a/lawnchair/src/app/lawnchair/gestures/GestureHandler.kt b/lawnchair/src/app/lawnchair/gestures/GestureHandler.kt
index 43b1b604bf..877ac28b48 100644
--- a/lawnchair/src/app/lawnchair/gestures/GestureHandler.kt
+++ b/lawnchair/src/app/lawnchair/gestures/GestureHandler.kt
@@ -23,3 +23,7 @@ abstract class GestureHandler(val context: Context) {
abstract suspend fun onTrigger(launcher: LawnchairLauncher)
}
+
+class NoOpGestureHandler(context: Context) : GestureHandler(context) {
+ override suspend fun onTrigger(launcher: LawnchairLauncher) = Unit
+}
diff --git a/lawnchair/src/app/lawnchair/gestures/config/GestureHandlerConfig.kt b/lawnchair/src/app/lawnchair/gestures/config/GestureHandlerConfig.kt
new file mode 100644
index 0000000000..4a2e24a8ac
--- /dev/null
+++ b/lawnchair/src/app/lawnchair/gestures/config/GestureHandlerConfig.kt
@@ -0,0 +1,41 @@
+package app.lawnchair.gestures.config
+
+import android.content.Context
+import app.lawnchair.gestures.GestureHandler
+import app.lawnchair.gestures.NoOpGestureHandler
+import app.lawnchair.gestures.handlers.OpenNotificationsHandler
+import app.lawnchair.gestures.handlers.SleepGestureHandler
+import com.android.launcher3.R
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+
+@Serializable
+sealed class GestureHandlerConfig {
+
+ abstract fun getLabel(context: Context): String
+ abstract fun createHandler(context: Context): GestureHandler
+
+ @Serializable
+ sealed class Simple(
+ private val labelRes: Int,
+ @Transient private val creator: (Context) -> GestureHandler = {
+ throw IllegalArgumentException("default creator not supported")
+ }
+ ) : GestureHandlerConfig() {
+ override fun getLabel(context: Context) = context.getString(labelRes)
+ override fun createHandler(context: Context) = creator(context)
+ }
+
+ @Serializable
+ @SerialName("noOp")
+ object NoOp : Simple(R.string.gesture_handler_no_op, { NoOpGestureHandler(it) })
+
+ @Serializable
+ @SerialName("sleep")
+ object Sleep : Simple(R.string.gesture_handler_sleep, { SleepGestureHandler(it) })
+
+ @Serializable
+ @SerialName("openNotifications")
+ object OpenNotifications : Simple(R.string.gesture_handler_open_notifications, { OpenNotificationsHandler(it) })
+}
diff --git a/lawnchair/src/app/lawnchair/gestures/config/GestureHandlerOption.kt b/lawnchair/src/app/lawnchair/gestures/config/GestureHandlerOption.kt
new file mode 100644
index 0000000000..823e6a389d
--- /dev/null
+++ b/lawnchair/src/app/lawnchair/gestures/config/GestureHandlerOption.kt
@@ -0,0 +1,22 @@
+package app.lawnchair.gestures.config
+
+import android.content.Context
+import com.android.launcher3.R
+
+abstract class GestureHandlerOption(
+ val labelRes: Int,
+ val configClass: Class<*>
+) {
+
+ fun getLabel(context: Context) = context.getString(labelRes)
+
+ abstract suspend fun buildConfig(): GestureHandlerConfig
+
+ open class Simple(labelRes: Int, val obj: GestureHandlerConfig) : GestureHandlerOption(labelRes, obj::class.java) {
+ override suspend fun buildConfig() = obj
+ }
+
+ object NoOp : Simple(R.string.gesture_handler_no_op, GestureHandlerConfig.NoOp)
+ object Sleep : Simple(R.string.gesture_handler_sleep, GestureHandlerConfig.Sleep)
+ object OpenNotifications : Simple(R.string.gesture_handler_open_notifications, GestureHandlerConfig.OpenNotifications)
+}
diff --git a/lawnchair/src/app/lawnchair/gestures/handlers/OpenNotificationsHandler.kt b/lawnchair/src/app/lawnchair/gestures/handlers/OpenNotificationsHandler.kt
new file mode 100644
index 0000000000..9bdaa1ec05
--- /dev/null
+++ b/lawnchair/src/app/lawnchair/gestures/handlers/OpenNotificationsHandler.kt
@@ -0,0 +1,27 @@
+package app.lawnchair.gestures.handlers
+
+import android.annotation.SuppressLint
+import android.content.Context
+import app.lawnchair.LawnchairLauncher
+import app.lawnchair.gestures.GestureHandler
+import java.lang.reflect.InvocationTargetException
+
+class OpenNotificationsHandler(context: Context) : GestureHandler(context) {
+
+ @SuppressLint("WrongConstant")
+ override suspend fun onTrigger(launcher: LawnchairLauncher) {
+ try {
+ Class.forName("android.app.StatusBarManager")
+ .getMethod("expandNotificationsPanel")
+ .invoke(context.getSystemService("statusbar"))
+ } catch (e: IllegalAccessException) {
+ e.printStackTrace()
+ } catch (e: InvocationTargetException) {
+ e.printStackTrace()
+ } catch (e: NoSuchMethodException) {
+ e.printStackTrace()
+ } catch (e: ClassNotFoundException) {
+ e.printStackTrace()
+ }
+ }
+}
diff --git a/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt b/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt
index ef4b03861f..7c6c1617d7 100644
--- a/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt
+++ b/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt
@@ -22,6 +22,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import app.lawnchair.font.FontCache
+import app.lawnchair.gestures.config.GestureHandlerConfig
import app.lawnchair.icons.CustomAdaptiveIconDrawable
import app.lawnchair.icons.shape.IconShape
import app.lawnchair.icons.shape.IconShapeManager
@@ -41,6 +42,9 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
import app.lawnchair.preferences.PreferenceManager as LawnchairPreferenceManager
import com.android.launcher3.graphics.IconShape as L3IconShape
@@ -160,11 +164,6 @@ class PreferenceManager2(private val context: Context) : PreferenceManager {
defaultValue = context.resources.getBoolean(R.bool.config_default_enable_smartspace_calendar_selection),
)
- val dt2s = preference(
- key = booleanPreferencesKey(name = "dt2s"),
- defaultValue = context.resources.getBoolean(R.bool.config_default_dts2),
- )
-
val autoShowKeyboardInDrawer = preference(
key = booleanPreferencesKey(name = "auto_show_keyboard_in_drawer"),
defaultValue = context.resources.getBoolean(R.bool.config_default_auto_show_keyboard_in_drawer),
@@ -304,6 +303,21 @@ class PreferenceManager2(private val context: Context) : PreferenceManager {
save = { it.toString() },
)
+ val doubleTapHandler = serializablePreference(
+ key = stringPreferencesKey("double_tap_handler"),
+ defaultValue = GestureHandlerConfig.Sleep
+ )
+
+ private inline fun serializablePreference(
+ key: Preferences.Key,
+ defaultValue: T
+ ) = preference(
+ key = key,
+ defaultValue = defaultValue,
+ parse = { Json.decodeFromString(it) },
+ save = { Json.encodeToString(it) }
+ )
+
init {
initializeIconShape(iconShape.firstBlocking())
iconShape.get()
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/HomeScreenPreferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/HomeScreenPreferences.kt
index 35f059897a..6ff1aa4f7b 100644
--- a/lawnchair/src/app/lawnchair/ui/preferences/HomeScreenPreferences.kt
+++ b/lawnchair/src/app/lawnchair/ui/preferences/HomeScreenPreferences.kt
@@ -16,8 +16,10 @@
package app.lawnchair.ui.preferences
-import androidx.compose.animation.*
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavGraphBuilder
@@ -57,9 +59,9 @@ fun HomeScreenPreferences() {
prefs.wallpaperScrolling.getAdapter(),
label = stringResource(id = R.string.wallpaper_scrolling_label),
)
- SwitchPreference(
- adapter = prefs2.dt2s.getAdapter(),
- label = stringResource(id = R.string.workspace_dt2s),
+ GestureHandlerPreference(
+ adapter = prefs2.doubleTapHandler.getAdapter(),
+ label = stringResource(id = R.string.gesture_double_tap)
)
val feedAvailable = OverlayCallbackImpl.minusOneAvailable(LocalContext.current)
SwitchPreference(
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/GestureHandlerPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/GestureHandlerPreference.kt
new file mode 100644
index 0000000000..4258f96080
--- /dev/null
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/GestureHandlerPreference.kt
@@ -0,0 +1,83 @@
+package app.lawnchair.ui.preferences.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material.RadioButton
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import app.lawnchair.gestures.config.GestureHandlerConfig
+import app.lawnchair.gestures.config.GestureHandlerOption
+import app.lawnchair.preferences.PreferenceAdapter
+import app.lawnchair.ui.AlertBottomSheetContent
+import app.lawnchair.ui.util.LocalBottomSheetHandler
+import kotlinx.coroutines.launch
+
+val options = listOf(
+ GestureHandlerOption.NoOp,
+ GestureHandlerOption.Sleep,
+ GestureHandlerOption.OpenNotifications,
+)
+
+@Composable
+fun GestureHandlerPreference(
+ adapter: PreferenceAdapter,
+ label: String
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val bottomSheetHandler = LocalBottomSheetHandler.current
+
+ val currentConfig = adapter.state.value
+
+ fun onSelect(option: GestureHandlerOption) {
+ scope.launch {
+ adapter.onChange(option.buildConfig())
+ }
+ }
+
+ PreferenceTemplate(
+ title = { Text(text = label) },
+ description = { Text(text = currentConfig.getLabel(context)) },
+ modifier = Modifier.clickable {
+ bottomSheetHandler.show {
+ AlertBottomSheetContent(
+ title = { Text(label) },
+ buttons = {
+ OutlinedButton(onClick = { bottomSheetHandler.hide() }) {
+ Text(text = stringResource(id = android.R.string.cancel))
+ }
+ }
+ ) {
+ LazyColumn {
+ itemsIndexed(options) { index, option ->
+ if (index > 0) {
+ PreferenceDivider(startIndent = 40.dp)
+ }
+ val selected = currentConfig::class.java == option.configClass
+ PreferenceTemplate(
+ title = { Text(option.getLabel(context)) },
+ modifier = Modifier.clickable {
+ bottomSheetHandler.hide()
+ onSelect(option)
+ },
+ startWidget = {
+ RadioButton(
+ selected = selected,
+ onClick = null
+ )
+ },
+ )
+ }
+ }
+ }
+ }
+ }
+ )
+}