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 + ) + }, + ) + } + } + } + } + } + ) +}