diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt
index 67c5e52..460161c 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt
@@ -16,6 +16,7 @@ import de.robv.android.xposed.XposedHelpers
import ru.hepolise.volumekeytrackcontrol.module.util.LogHelper
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLongPressDuration
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.isSwapButtons
import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.getVibrator
import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.triggerVibration
@@ -150,17 +151,25 @@ object VolumeKeyControlModuleHandlers {
}
private fun doHook(keyCode: Int, event: KeyEvent, param: MethodHookParam) {
+ val isSwap = SharedPreferencesUtil.prefs().isSwapButtons()
+ log("isSwap: $isSwap")
+ val isDown = when (keyCode) {
+ KeyEvent.KEYCODE_VOLUME_DOWN -> !isSwap
+ KeyEvent.KEYCODE_VOLUME_UP -> isSwap
+ else -> throw IllegalStateException("Unknown key code: $keyCode")
+ }
when (event.action) {
- KeyEvent.ACTION_DOWN -> handleDownAction(keyCode, param)
- KeyEvent.ACTION_UP -> handleUpAction(keyCode, param)
+ KeyEvent.ACTION_DOWN -> handleDownAction(isDown, param)
+ KeyEvent.ACTION_UP -> handleUpAction(isDown, keyCode, param)
}
param.setResult(0)
}
- private fun handleDownAction(keyCode: Int, param: MethodHookParam) {
- when (keyCode) {
- KeyEvent.KEYCODE_VOLUME_DOWN -> isDownPressed = true
- KeyEvent.KEYCODE_VOLUME_UP -> isUpPressed = true
+ private fun handleDownAction(isDown: Boolean, param: MethodHookParam) {
+ if (isDown) {
+ isDownPressed = true
+ } else {
+ isUpPressed = true
}
log("down action received, down: $isDownPressed, up: $isUpPressed")
isLongPress = false
@@ -171,17 +180,18 @@ object VolumeKeyControlModuleHandlers {
// only one button pressed
if (isMusicActive()) {
log("music is active, creating delayed skip")
- handleVolumeSkipPress(param.thisObject, keyCode)
+ handleVolumeSkipPress(param.thisObject, isDown)
}
log("creating delayed play pause")
handleVolumePlayPausePress(param.thisObject)
}
}
- private fun handleUpAction(keyCode: Int, param: MethodHookParam) {
- when (keyCode) {
- KeyEvent.KEYCODE_VOLUME_DOWN -> isDownPressed = false
- KeyEvent.KEYCODE_VOLUME_UP -> isUpPressed = false
+ private fun handleUpAction(isDown: Boolean, keyCode: Int, param: MethodHookParam) {
+ if (isDown) {
+ isDownPressed = false
+ } else {
+ isUpPressed = false
}
log("up action received, down: $isDownPressed, up: $isUpPressed")
handleVolumeAllPressAbort(param.thisObject)
@@ -235,14 +245,10 @@ object VolumeKeyControlModuleHandlers {
)
}
- private fun handleVolumeSkipPress(instance: Any, keyCode: Int) {
+ private fun handleVolumeSkipPress(instance: Any, isDown: Boolean) {
val handler = instance.getHandler()
handler.postDelayed(
- when (keyCode) {
- KeyEvent.KEYCODE_VOLUME_UP -> getRunnable(instance, VOLUME_UP_LONG_PRESS)
- KeyEvent.KEYCODE_VOLUME_DOWN -> getRunnable(instance, VOLUME_DOWN_LONG_PRESS)
- else -> return
- },
+ getRunnable(instance, if (isDown) VOLUME_DOWN_LONG_PRESS else VOLUME_UP_LONG_PRESS),
SharedPreferencesUtil.prefs().getLongPressDuration().toLong()
)
}
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/SettingsActivity.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/SettingsActivity.kt
index a30a59e..0055edf 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/SettingsActivity.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/SettingsActivity.kt
@@ -28,35 +28,22 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ColorScheme
-import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.MenuAnchorType
-import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
-import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -64,39 +51,32 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.LinkAnnotation
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.TextLinkStyles
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.text.style.TextDecoration
-import androidx.compose.ui.text.withLink
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.animation.doOnEnd
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import ru.hepolise.volumekeytrackcontrol.ui.component.LongPressSetting
+import ru.hepolise.volumekeytrackcontrol.ui.component.ModuleIsNotEnabled
+import ru.hepolise.volumekeytrackcontrol.ui.component.SwapButtonsSetting
+import ru.hepolise.volumekeytrackcontrol.ui.component.VibrationEffectSetting
+import ru.hepolise.volumekeytrackcontrol.ui.component.VibrationSettingData
import ru.hepolise.volumekeytrackcontrol.util.Constants
-import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LONG_PRESS_DURATION
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.IS_SWAP_BUTTONS_DEFAULT_VALUE
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LONG_PRESS_DURATION_DEFAULT_VALUE
-import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SELECTED_EFFECT
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SELECTED_EFFECT_DEFAULT_VALUE
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SETTINGS_PREFS_NAME
-import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_AMPLITUDE
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_AMPLITUDE_DEFAULT_VALUE
-import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_LENGTH
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_LENGTH_DEFAULT_VALUE
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLongPressDuration
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getVibrationAmplitude
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getVibrationLength
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getVibrationType
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.isSwapButtons
import ru.hepolise.volumekeytrackcontrol.util.VibrationType
import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.getVibrator
-import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.triggerVibration
import ru.hepolise.volumekeytrackcontrolmodule.R
@@ -141,26 +121,6 @@ class SettingsActivity : ComponentActivity() {
}
}
-private val VibrationEffectTitles = VibrationType.values.associateWith {
- when (it) {
- VibrationType.Disabled -> R.string.vibration_disabled
- VibrationType.Manual -> R.string.vibration_manual
- else -> {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- when (it) {
- VibrationType.Click -> R.string.vibration_effect_click
- VibrationType.DoubleClick -> R.string.vibration_effect_double_click
- VibrationType.HeavyClick -> R.string.vibration_effect_heavy_click
- VibrationType.Tick -> R.string.vibration_effect_tick
- else -> throw IllegalStateException("Unknown VibrationType: $it")
- }
- } else {
- throw IllegalStateException("VibrationType is not supported on this API level")
- }
- }
- }
-}
-
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VibrationSettingsScreen(vibrator: Vibrator?) {
@@ -178,6 +138,7 @@ fun VibrationSettingsScreen(vibrator: Vibrator?) {
var vibrationType by remember { mutableStateOf(sharedPreferences.getVibrationType()) }
var vibrationLength by remember { mutableIntStateOf(sharedPreferences.getVibrationLength()) }
var vibrationAmplitude by remember { mutableIntStateOf(sharedPreferences.getVibrationAmplitude()) }
+ var isSwapButtons by remember { mutableStateOf(sharedPreferences.isSwapButtons()) }
Scaffold(
topBar = {
@@ -199,184 +160,39 @@ fun VibrationSettingsScreen(vibrator: Vibrator?) {
horizontalAlignment = Alignment.CenterHorizontally
) {
- Text(text = stringResource(R.string.long_press_settings), fontSize = 20.sp)
-
- Slider(
- value = longPressDuration.toFloat(),
- onValueChange = {
- longPressDuration = it.toInt()
- },
- valueRange = 100f..1000f,
- onValueChangeFinished = {
- sharedPreferences.edit().putInt(LONG_PRESS_DURATION, longPressDuration)
- .apply()
- },
- modifier = Modifier.widthIn(max = 300.dp)
- )
-
- var showLongPressTimeoutDialog by remember { mutableStateOf(false) }
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(stringResource(R.string.long_press_duration, longPressDuration))
- IconButton(
- onClick = {
- showLongPressTimeoutDialog = true
- }
- ) {
- Icon(
- imageVector = Icons.Default.Edit,
- contentDescription = stringResource(R.string.edit)
- )
- }
- }
-
- if (showLongPressTimeoutDialog) {
- NumberAlertDialog(
- title = stringResource(R.string.long_press_duration_dialog_title),
- defaultValue = longPressDuration,
- minValue = 100,
- maxValue = 1000,
- onDismissRequest = { showLongPressTimeoutDialog = false },
- onConfirm = {
- longPressDuration = it
- sharedPreferences.edit().putInt(LONG_PRESS_DURATION, it).apply()
- showLongPressTimeoutDialog = false
- }
- )
+ LongPressSetting(longPressDuration, sharedPreferences) {
+ longPressDuration = it
}
HorizontalDivider(modifier = Modifier.widthIn(max = 300.dp))
- Text(text = stringResource(R.string.vibration_settings), fontSize = 20.sp)
-
- var effectExpanded by remember { mutableStateOf(false) }
- ExposedDropdownMenuBox(
- expanded = effectExpanded,
- onExpandedChange = { effectExpanded = !effectExpanded }
+ VibrationEffectSetting(
+ value = VibrationSettingData(
+ vibrationType, vibrationLength, vibrationAmplitude
+ ),
+ vibrator = vibrator,
+ sharedPreferences = sharedPreferences
) {
- TextField(
- value = stringResource(VibrationEffectTitles[vibrationType]!!),
- onValueChange = {},
- readOnly = true,
- trailingIcon = {
- ExposedDropdownMenuDefaults.TrailingIcon(expanded = effectExpanded)
- },
- modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable)
- )
- ExposedDropdownMenu(
- expanded = effectExpanded,
- onDismissRequest = { effectExpanded = false }) {
- VibrationType.values.forEach { effect ->
- DropdownMenuItem(
- text = { Text(stringResource(VibrationEffectTitles[effect]!!)) },
- onClick = {
- vibrationType = effect
- sharedPreferences.edit().putString(SELECTED_EFFECT, effect.key)
- .apply()
- effectExpanded = false
- }
- )
- }
- }
+ vibrationType = it.vibrationType
+ vibrationLength = it.vibrationLength
+ vibrationAmplitude = it.vibrationAmplitude
}
- if (vibrationType == VibrationType.Manual) {
- Slider(
- value = vibrationLength.toFloat(),
- onValueChange = {
- vibrationLength = it.toInt()
- },
- valueRange = 10f..500f,
- onValueChangeFinished = {
- sharedPreferences.edit().putInt(VIBRATION_LENGTH, vibrationLength)
- .apply()
- },
- modifier = Modifier.widthIn(max = 300.dp)
- )
-
- var showManualVibrationLengthDialog by remember { mutableStateOf(false) }
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(stringResource(R.string.vibration_length, vibrationLength))
- IconButton(
- onClick = {
- showManualVibrationLengthDialog = true
- }
- ) {
- Icon(
- imageVector = Icons.Default.Edit,
- contentDescription = stringResource(R.string.edit)
- )
- }
- }
-
- if (showManualVibrationLengthDialog) {
- NumberAlertDialog(
- title = stringResource(R.string.vibration_length_dialog_title),
- defaultValue = vibrationLength,
- minValue = 10,
- maxValue = 500,
- onDismissRequest = { showManualVibrationLengthDialog = false },
- onConfirm = {
- vibrationLength = it
- sharedPreferences.edit().putInt(VIBRATION_LENGTH, it).apply()
- showManualVibrationLengthDialog = false
- }
- )
- }
- Slider(
- value = vibrationAmplitude.toFloat(),
- onValueChange = {
- vibrationAmplitude = it.toInt()
- },
- valueRange = 1f..255f,
- onValueChangeFinished = {
- sharedPreferences.edit().putInt(VIBRATION_AMPLITUDE, vibrationAmplitude)
- .apply()
- },
- modifier = Modifier.widthIn(max = 300.dp)
- )
- var showVibrationAmplitudeDialog by remember { mutableStateOf(false) }
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(stringResource(R.string.vibration_amplitude, vibrationAmplitude))
- IconButton(
- onClick = {
- showVibrationAmplitudeDialog = true
- }
- ) {
- Icon(
- imageVector = Icons.Default.Edit,
- contentDescription = stringResource(R.string.edit)
- )
- }
- }
+ HorizontalDivider(modifier = Modifier.widthIn(max = 300.dp))
- if (showVibrationAmplitudeDialog) {
- NumberAlertDialog(
- title = stringResource(R.string.vibration_amplitude_dialog_title),
- defaultValue = vibrationAmplitude,
- minValue = 1,
- maxValue = 255,
- onDismissRequest = { showVibrationAmplitudeDialog = false },
- onConfirm = {
- vibrationAmplitude = it
- sharedPreferences.edit().putInt(VIBRATION_AMPLITUDE, it)
- .apply()
- showVibrationAmplitudeDialog = false
- }
- )
- }
+ Text(text = stringResource(R.string.other_settings), fontSize = 20.sp)
+ SwapButtonsSetting(
+ isSwapButtons = isSwapButtons,
+ sharedPreferences = sharedPreferences
+ ) {
+ isSwapButtons = it
}
- if (vibrationType != VibrationType.Disabled) {
- Button(onClick = {
- vibrator?.triggerVibration(sharedPreferences)
- }) {
- Text(stringResource(R.string.test_vibration))
- }
- }
}
+
Row(
modifier = Modifier
.fillMaxWidth()
@@ -405,6 +221,7 @@ fun VibrationSettingsScreen(vibrator: Vibrator?) {
vibrationLength = VIBRATION_LENGTH_DEFAULT_VALUE
vibrationAmplitude = VIBRATION_AMPLITUDE_DEFAULT_VALUE
longPressDuration = LONG_PRESS_DURATION_DEFAULT_VALUE
+ isSwapButtons = IS_SWAP_BUTTONS_DEFAULT_VALUE
Toast.makeText(
context,
context.getString(R.string.settings_reset_toast),
@@ -449,97 +266,6 @@ fun dynamicColorScheme(context: Context): ColorScheme {
}
}
-@Composable
-fun NumberAlertDialog(
- title: String,
- defaultValue: Int,
- onDismissRequest: () -> Unit,
- onConfirm: (Int) -> Unit,
- minValue: Int,
- maxValue: Int
-) {
- fun validate(value: Int) = value in minValue..maxValue
- var value by remember { mutableStateOf(defaultValue.toString()) }
- val focusRequester = remember { FocusRequester() }
- AlertDialog(
- onDismissRequest = onDismissRequest,
- title = { Text(text = title) },
- text = {
- Column {
- OutlinedTextField(
- value = value,
- onValueChange = { value = it },
- label = { Text(stringResource(R.string.value_in_range, minValue, maxValue)) },
- keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
- isError = value.toIntOrNull() == null || !validate(value.toInt()),
- modifier = Modifier.focusRequester(focusRequester)
- )
- }
- },
- confirmButton = {
- Button(
- onClick = {
- onConfirm(value.toInt())
- },
- enabled = value.toIntOrNull() != null && validate(value.toInt())
- ) {
- Text(text = stringResource(R.string.ok))
- }
- },
- dismissButton = {
- TextButton(onClick = onDismissRequest) {
- Text(text = stringResource(R.string.cancel))
- }
- },
- )
- LaunchedEffect(true) {
- focusRequester.requestFocus()
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun ModuleIsNotEnabled() {
- Scaffold(
- topBar = {
- TopAppBar(title = { Text(stringResource(R.string.app_name)) })
- }
- ) { padding ->
- Box(
- modifier = Modifier
- .fillMaxSize()
- ) {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(padding),
- verticalArrangement = Arrangement.spacedBy(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = buildAnnotatedString {
- append(stringResource(id = R.string.module_is_not_enabled))
- append(" ")
- withLink(
- LinkAnnotation.Url(
- url = Constants.GITHUB_NEW_ISSUE,
- styles = TextLinkStyles(
- style = SpanStyle(
- color = MaterialTheme.colorScheme.primary,
- textDecoration = TextDecoration.Underline
- )
- )
- )
- ) {
- append(stringResource(id = R.string.open_issue))
- }
- }
- )
- }
- }
- }
-}
-
@Preview(showBackground = true)
@Composable
fun PreviewVibrationSettingsScreen() {
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/LongPressSetting.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/LongPressSetting.kt
new file mode 100644
index 0000000..871626e
--- /dev/null
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/LongPressSetting.kt
@@ -0,0 +1,78 @@
+package ru.hepolise.volumekeytrackcontrol.ui.component
+
+import android.content.SharedPreferences
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LONG_PRESS_DURATION
+import ru.hepolise.volumekeytrackcontrolmodule.R
+
+@Composable
+fun LongPressSetting(
+ longPressDuration: Int,
+ sharedPreferences: SharedPreferences,
+ onValueChange: (Int) -> Unit
+) {
+
+ Text(text = stringResource(R.string.long_press_settings), fontSize = 20.sp)
+
+ Slider(
+ value = longPressDuration.toFloat(),
+ onValueChange = { onValueChange(it.toInt()) },
+ valueRange = 100f..1000f,
+ onValueChangeFinished = {
+ sharedPreferences.edit().putInt(LONG_PRESS_DURATION, longPressDuration)
+ .apply()
+ },
+ modifier = Modifier.widthIn(max = 300.dp)
+ )
+
+ var showLongPressTimeoutDialog by remember { mutableStateOf(false) }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = stringResource(R.string.long_press_duration, longPressDuration),
+ modifier = Modifier.clickable { showLongPressTimeoutDialog = true }
+ )
+ IconButton(
+ onClick = {
+ showLongPressTimeoutDialog = true
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Default.Edit,
+ contentDescription = stringResource(R.string.edit)
+ )
+ }
+ }
+
+ if (showLongPressTimeoutDialog) {
+ NumberAlertDialog(
+ title = stringResource(R.string.long_press_duration_dialog_title),
+ defaultValue = longPressDuration,
+ minValue = 100,
+ maxValue = 1000,
+ onDismissRequest = { showLongPressTimeoutDialog = false },
+ onConfirm = {
+ onValueChange(it)
+ sharedPreferences.edit().putInt(LONG_PRESS_DURATION, it).apply()
+ showLongPressTimeoutDialog = false
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/ModuleIsNotEnabled.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/ModuleIsNotEnabled.kt
new file mode 100644
index 0000000..14ca231
--- /dev/null
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/ModuleIsNotEnabled.kt
@@ -0,0 +1,68 @@
+package ru.hepolise.volumekeytrackcontrol.ui.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.LinkAnnotation
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextLinkStyles
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.withLink
+import androidx.compose.ui.unit.dp
+import ru.hepolise.volumekeytrackcontrol.util.Constants
+import ru.hepolise.volumekeytrackcontrolmodule.R
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ModuleIsNotEnabled() {
+ Scaffold(
+ topBar = {
+ TopAppBar(title = { Text(stringResource(R.string.app_name)) })
+ }
+ ) { padding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = buildAnnotatedString {
+ append(stringResource(id = R.string.module_is_not_enabled))
+ append(" ")
+ withLink(
+ LinkAnnotation.Url(
+ url = Constants.GITHUB_NEW_ISSUE,
+ styles = TextLinkStyles(
+ style = SpanStyle(
+ color = MaterialTheme.colorScheme.primary,
+ textDecoration = TextDecoration.Underline
+ )
+ )
+ )
+ ) {
+ append(stringResource(id = R.string.open_issue))
+ }
+ }
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/NumberAlertDialog.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/NumberAlertDialog.kt
new file mode 100644
index 0000000..4588ac5
--- /dev/null
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/NumberAlertDialog.kt
@@ -0,0 +1,69 @@
+package ru.hepolise.volumekeytrackcontrol.ui.component
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardType
+import ru.hepolise.volumekeytrackcontrolmodule.R
+
+@Composable
+fun NumberAlertDialog(
+ title: String,
+ defaultValue: Int,
+ onDismissRequest: () -> Unit,
+ onConfirm: (Int) -> Unit,
+ minValue: Int,
+ maxValue: Int
+) {
+ fun validate(value: Int) = value in minValue..maxValue
+ var value by remember { mutableStateOf(defaultValue.toString()) }
+ val focusRequester = remember { FocusRequester() }
+ AlertDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(text = title) },
+ text = {
+ Column {
+ OutlinedTextField(
+ value = value,
+ onValueChange = { value = it },
+ label = { Text(stringResource(R.string.value_in_range, minValue, maxValue)) },
+ keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
+ isError = value.toIntOrNull() == null || !validate(value.toInt()),
+ modifier = Modifier.focusRequester(focusRequester)
+ )
+ }
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ onConfirm(value.toInt())
+ },
+ enabled = value.toIntOrNull() != null && validate(value.toInt())
+ ) {
+ Text(text = stringResource(R.string.ok))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(text = stringResource(R.string.cancel))
+ }
+ },
+ )
+ LaunchedEffect(true) {
+ focusRequester.requestFocus()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/SwapButtonsSetting.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/SwapButtonsSetting.kt
new file mode 100644
index 0000000..3763408
--- /dev/null
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/SwapButtonsSetting.kt
@@ -0,0 +1,41 @@
+package ru.hepolise.volumekeytrackcontrol.ui.component
+
+import android.content.SharedPreferences
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.IS_SWAP_BUTTONS
+import ru.hepolise.volumekeytrackcontrolmodule.R
+
+@Composable
+fun SwapButtonsSetting(
+ isSwapButtons: Boolean,
+ sharedPreferences: SharedPreferences,
+ onValueChange: (Boolean) -> Unit
+) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Checkbox(
+ checked = isSwapButtons,
+ onCheckedChange = {
+ onValueChange(it)
+ sharedPreferences.edit().putBoolean(IS_SWAP_BUTTONS, it).apply()
+ }
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = stringResource(R.string.swap_buttons),
+ modifier = Modifier.clickable {
+ onValueChange(!isSwapButtons)
+ sharedPreferences.edit().putBoolean(IS_SWAP_BUTTONS, !isSwapButtons).apply()
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/VibrationEffectSetting.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/VibrationEffectSetting.kt
new file mode 100644
index 0000000..275c908
--- /dev/null
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/VibrationEffectSetting.kt
@@ -0,0 +1,211 @@
+package ru.hepolise.volumekeytrackcontrol.ui.component
+
+import android.content.SharedPreferences
+import android.os.Build
+import android.os.Vibrator
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.Button
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MenuAnchorType
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SELECTED_EFFECT
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_AMPLITUDE
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_LENGTH
+import ru.hepolise.volumekeytrackcontrol.util.VibrationType
+import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.triggerVibration
+import ru.hepolise.volumekeytrackcontrolmodule.R
+
+data class VibrationSettingData(
+ val vibrationType: VibrationType,
+ val vibrationLength: Int,
+ val vibrationAmplitude: Int
+)
+
+private val VibrationEffectTitles = VibrationType.values.associateWith {
+ when (it) {
+ VibrationType.Disabled -> R.string.vibration_disabled
+ VibrationType.Manual -> R.string.vibration_manual
+ else -> {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ when (it) {
+ VibrationType.Click -> R.string.vibration_effect_click
+ VibrationType.DoubleClick -> R.string.vibration_effect_double_click
+ VibrationType.HeavyClick -> R.string.vibration_effect_heavy_click
+ VibrationType.Tick -> R.string.vibration_effect_tick
+ else -> throw IllegalStateException("Unknown VibrationType: $it")
+ }
+ } else {
+ throw IllegalStateException("VibrationType is not supported on this API level")
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun VibrationEffectSetting(
+ value: VibrationSettingData,
+ vibrator: Vibrator?,
+ sharedPreferences: SharedPreferences,
+ onValueChange: (VibrationSettingData) -> Unit
+) {
+ val (vibrationType, vibrationLength, vibrationAmplitude) = value
+ Text(text = stringResource(R.string.vibration_settings), fontSize = 20.sp)
+
+ var effectExpanded by remember { mutableStateOf(false) }
+ ExposedDropdownMenuBox(
+ expanded = effectExpanded,
+ onExpandedChange = { effectExpanded = !effectExpanded }
+ ) {
+ TextField(
+ value = stringResource(VibrationEffectTitles[vibrationType]!!),
+ onValueChange = {},
+ readOnly = true,
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(expanded = effectExpanded)
+ },
+ modifier = Modifier
+ .menuAnchor(MenuAnchorType.PrimaryNotEditable)
+ )
+ ExposedDropdownMenu(
+ expanded = effectExpanded,
+ onDismissRequest = { effectExpanded = false }) {
+ VibrationType.values.forEach { effect ->
+ DropdownMenuItem(
+ text = { Text(stringResource(VibrationEffectTitles[effect]!!)) },
+ onClick = {
+ onValueChange(value.copy(vibrationType = effect))
+ sharedPreferences.edit().putString(SELECTED_EFFECT, effect.key)
+ .apply()
+ effectExpanded = false
+ }
+ )
+ }
+ }
+ }
+
+ if (vibrationType == VibrationType.Manual) {
+ Slider(
+ value = vibrationLength.toFloat(),
+ onValueChange = {
+ onValueChange(value.copy(vibrationLength = it.toInt()))
+ },
+ valueRange = 10f..500f,
+ onValueChangeFinished = {
+ sharedPreferences.edit().putInt(VIBRATION_LENGTH, vibrationLength)
+ .apply()
+ },
+ modifier = Modifier.widthIn(max = 300.dp)
+ )
+
+ var showManualVibrationLengthDialog by remember { mutableStateOf(false) }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = stringResource(R.string.vibration_length, vibrationLength),
+ modifier = Modifier.clickable { showManualVibrationLengthDialog = true }
+ )
+ IconButton(
+ onClick = {
+ showManualVibrationLengthDialog = true
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Default.Edit,
+ contentDescription = stringResource(R.string.edit)
+ )
+ }
+ }
+
+ if (showManualVibrationLengthDialog) {
+ NumberAlertDialog(
+ title = stringResource(R.string.vibration_length_dialog_title),
+ defaultValue = vibrationLength,
+ minValue = 10,
+ maxValue = 500,
+ onDismissRequest = { showManualVibrationLengthDialog = false },
+ onConfirm = {
+ onValueChange(value.copy(vibrationLength = it))
+ sharedPreferences.edit().putInt(VIBRATION_LENGTH, it).apply()
+ showManualVibrationLengthDialog = false
+ }
+ )
+ }
+
+ Slider(
+ value = vibrationAmplitude.toFloat(),
+ onValueChange = {
+ onValueChange(value.copy(vibrationAmplitude = it.toInt()))
+ },
+ valueRange = 1f..255f,
+ onValueChangeFinished = {
+ sharedPreferences.edit().putInt(VIBRATION_AMPLITUDE, vibrationAmplitude)
+ .apply()
+ },
+ modifier = Modifier.widthIn(max = 300.dp)
+ )
+
+ var showVibrationAmplitudeDialog by remember { mutableStateOf(false) }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = stringResource(R.string.vibration_amplitude, vibrationAmplitude),
+ modifier = Modifier.clickable { showVibrationAmplitudeDialog = true }
+ )
+ IconButton(
+ onClick = {
+ showVibrationAmplitudeDialog = true
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Default.Edit,
+ contentDescription = stringResource(R.string.edit)
+ )
+ }
+ }
+
+ if (showVibrationAmplitudeDialog) {
+ NumberAlertDialog(
+ title = stringResource(R.string.vibration_amplitude_dialog_title),
+ defaultValue = vibrationAmplitude,
+ minValue = 1,
+ maxValue = 255,
+ onDismissRequest = { showVibrationAmplitudeDialog = false },
+ onConfirm = {
+ onValueChange(value.copy(vibrationAmplitude = it))
+ sharedPreferences.edit().putInt(VIBRATION_AMPLITUDE, it)
+ .apply()
+ showVibrationAmplitudeDialog = false
+ }
+ )
+ }
+
+ }
+
+ if (vibrationType != VibrationType.Disabled) {
+ Button(onClick = {
+ vibrator?.triggerVibration(sharedPreferences)
+ }) {
+ Text(stringResource(R.string.test_vibration))
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SharedPreferencesUtil.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SharedPreferencesUtil.kt
index af477a4..33026e6 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SharedPreferencesUtil.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SharedPreferencesUtil.kt
@@ -13,12 +13,14 @@ object SharedPreferencesUtil {
const val VIBRATION_LENGTH = "vibrationLength"
const val VIBRATION_AMPLITUDE = "vibrationAmplitude"
const val LONG_PRESS_DURATION = "longPressDuration"
+ const val IS_SWAP_BUTTONS = "isSwapButtons"
val SELECTED_EFFECT_DEFAULT_VALUE =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) VibrationType.Click.key else VibrationType.Manual.key
const val VIBRATION_LENGTH_DEFAULT_VALUE = 50
const val VIBRATION_AMPLITUDE_DEFAULT_VALUE = 128
val LONG_PRESS_DURATION_DEFAULT_VALUE = ViewConfiguration.getLongPressTimeout()
+ const val IS_SWAP_BUTTONS_DEFAULT_VALUE = false
fun SharedPreferences?.getVibrationType(): VibrationType {
val defaultValue = SELECTED_EFFECT_DEFAULT_VALUE
@@ -40,6 +42,11 @@ object SharedPreferencesUtil {
return this?.getInt(LONG_PRESS_DURATION, defaultValue) ?: defaultValue
}
+ fun SharedPreferences?.isSwapButtons(): Boolean {
+ val defaultValue = IS_SWAP_BUTTONS_DEFAULT_VALUE
+ return this?.getBoolean(IS_SWAP_BUTTONS, defaultValue) ?: defaultValue
+ }
+
fun prefs(): SharedPreferences? {
val pref = XSharedPreferences(BuildConfig.APPLICATION_ID, SETTINGS_PREFS_NAME)
return if (pref.file.canRead()) pref else null
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8f4d87c..f0b1579 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -3,6 +3,7 @@
Vibration Settings
Long Press Settings
+ Other Settings
Click
Double Click
@@ -21,6 +22,8 @@
Long Press Duration: %dms
Long Press Duration
+ Swap Up/Down buttons for skipping
+
Reset settings
Are you sure you want to reset settings?
Settings are reset to default