diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c5e27e0..81091ee 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -35,7 +35,7 @@ jobs:
- uses: actions/checkout@v5
- name: Set up JDK 21
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index da86b28..ce42e9a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -42,14 +42,14 @@ android {
}
dependencies {
- implementation(platform("org.jetbrains.kotlin:kotlin-bom:2.2.10"))
- implementation("androidx.core:core-ktx:1.16.0")
+ implementation(platform("org.jetbrains.kotlin:kotlin-bom:2.2.20"))
+ implementation("androidx.core:core-ktx:1.17.0")
// Compose BOM (Bill of Materials)
- implementation(platform("androidx.compose:compose-bom:2025.08.00"))
+ implementation(platform("androidx.compose:compose-bom:2025.09.00"))
// Compose dependencies
- implementation("androidx.activity:activity-compose:1.10.1")
+ implementation("androidx.activity:activity-compose:1.11.0")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3:1.3.2")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cd0262f..353c273 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,6 +26,14 @@
+
+
+
+
+
+
tryHookInitMethod(classLoader, params, logMessage)
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 3279c83..f34eb0b 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt
@@ -1,6 +1,11 @@
package ru.hepolise.volumekeytrackcontrol.module
+import android.content.BroadcastReceiver
+import android.content.ComponentName
import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.SharedPreferences
import android.hardware.display.DisplayManager
import android.media.AudioManager
import android.media.session.MediaController
@@ -14,6 +19,8 @@ import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XC_MethodHook.MethodHookParam
import de.robv.android.xposed.XposedHelpers
import ru.hepolise.volumekeytrackcontrol.module.util.LogHelper
+import ru.hepolise.volumekeytrackcontrol.receiver.HookBroadcastReceiver
+import ru.hepolise.volumekeytrackcontrol.util.Constants
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getAppFilterType
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getApps
@@ -21,6 +28,7 @@ import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLongPress
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.isSwapButtons
import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.getVibrator
import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.triggerVibration
+import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig
object VolumeKeyControlModuleHandlers {
@@ -37,12 +45,15 @@ object VolumeKeyControlModuleHandlers {
private lateinit var displayManager: DisplayManager
private lateinit var vibrator: Vibrator
+ private var prefs: SharedPreferences? = null
+
private var mediaControllers: List? = null
private fun log(text: String) = LogHelper.log(VolumeControlModule::class.java.simpleName, text)
val handleInterceptKeyBeforeQueueing: XC_MethodHook = object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
+ initPrefs()
with(param) {
val event = args[0] as KeyEvent
try {
@@ -64,10 +75,22 @@ object VolumeKeyControlModuleHandlers {
val handleConstructPhoneWindowManager: XC_MethodHook = object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam) {
+ log("handleConstructPhoneWindowManager: initialized")
+ val context = param.getContext()
MediaEvent.entries.forEach { event ->
- val runnable = Runnable { event.handle() }
+ val runnable = Runnable { event.handle(context) }
XposedHelpers.setAdditionalInstanceField(param.thisObject, event.field, runnable)
}
+
+ val filter = IntentFilter(Intent.ACTION_USER_UNLOCKED)
+ context.registerReceiver(object : BroadcastReceiver() {
+ override fun onReceive(ctx: Context, intent: Intent) {
+ context.sendBroadcast {
+ putExtra(Constants.HOOKED, true)
+ }
+ ctx.unregisterReceiver(this)
+ }
+ }, filter)
}
}
@@ -117,6 +140,10 @@ object VolumeKeyControlModuleHandlers {
vibrator = getVibrator()
}
+ private fun initPrefs() {
+ prefs = SharedPreferencesUtil.prefs()
+ }
+
private fun Context.initControllers() {
val context = this
val classLoader = javaClass.classLoader
@@ -143,7 +170,7 @@ object VolumeKeyControlModuleHandlers {
}
private fun MethodHookParam.doHook(event: KeyEvent) {
- val action = Action.entries.find { it.actionCode == event.action }!!
+ val action = Action.entries.single { it.actionCode == event.action }
val keyHelper = KeyHelper(event.keyCode)
keyHelper.updateFlags(action)
when (action) {
@@ -160,7 +187,6 @@ object VolumeKeyControlModuleHandlers {
log("Aborting delayed skip")
abortSkip()
} else {
- // only one button pressed
if (getMediaController().isMusicActive()) {
log("Music is active, creating delayed skip")
delay(keyHelper.mediaEvent)
@@ -194,10 +220,9 @@ object VolumeKeyControlModuleHandlers {
}
private fun getMediaController(): MediaController? {
- val prefs = SharedPreferencesUtil.prefs()
+ val filterType = prefs.getAppFilterType()
+ val apps = prefs.getApps(filterType)
return mediaControllers?.find {
- val filterType = prefs.getAppFilterType()
- val apps = prefs.getApps(filterType)
when (filterType) {
SharedPreferencesUtil.AppFilterType.DISABLED -> true
SharedPreferencesUtil.AppFilterType.WHITE_LIST -> it.packageName in apps
@@ -230,11 +255,26 @@ object VolumeKeyControlModuleHandlers {
}
}
+ private fun Context.sendBroadcast(block: Intent.() -> Unit) {
+ val modulePackage = BuildConfig.APPLICATION_ID
+
+ val intent = Intent().apply {
+ component = ComponentName(
+ modulePackage,
+ HookBroadcastReceiver::class.java.name
+ )
+ action = Constants.HOOK_UPDATE
+ block()
+ setPackage(modulePackage)
+ }
+ sendBroadcast(intent)
+ }
+
private fun MethodHookParam.delay(event: MediaEvent) {
val handler = getHandler()
handler.postDelayed(
getRunnable(event.field),
- SharedPreferencesUtil.prefs().getLongPressDuration().toLong()
+ prefs.getLongPressDuration().toLong()
)
}
@@ -267,16 +307,19 @@ object VolumeKeyControlModuleHandlers {
private sealed class MediaEvent(val field: String) {
- open fun handle() {
+ open fun handle(context: Context) {
log("Sending ${this::class.simpleName}")
isLongPress = true
sendMediaButtonEventAndTriggerVibration(this)
+ context.sendBroadcast {
+ putExtra(Constants.INCREMENT_LAUNCH_COUNT, true)
+ }
}
object PlayPause : MediaEvent("mVolumeBothLongPress") {
- override fun handle() {
+ override fun handle(context: Context) {
if (isUpPressed && isDownPressed) {
- super.handle()
+ super.handle(context)
} else {
log("Not sending ${this::class.simpleName}, down: $isDownPressed, up: $isUpPressed")
}
@@ -295,7 +338,7 @@ object VolumeKeyControlModuleHandlers {
private class KeyHelper(keyCode: Int) {
private val origKey = Key.entries.find { it.keyCode == keyCode }!!
- private val isSwap = SharedPreferencesUtil.prefs().isSwapButtons()
+ private val isSwap = prefs.isSwapButtons()
private val key = if (isSwap) {
when (origKey) {
Key.UP -> Key.DOWN
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/HookBroadcastReceiver.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/HookBroadcastReceiver.kt
new file mode 100644
index 0000000..3ee9e2f
--- /dev/null
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/receiver/HookBroadcastReceiver.kt
@@ -0,0 +1,30 @@
+package ru.hepolise.volumekeytrackcontrol.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.core.content.edit
+import ru.hepolise.volumekeytrackcontrol.util.Constants
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.HOOK_PREFS_NAME
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LAST_INIT_HOOK_TIME
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LAUNCHED_COUNT
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLaunchedCount
+
+class HookBroadcastReceiver : BroadcastReceiver() {
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == Constants.HOOK_UPDATE) {
+ val prefs = context.getSharedPreferences(HOOK_PREFS_NAME, Context.MODE_PRIVATE)
+ prefs.edit {
+ if (intent.getBooleanExtra(Constants.HOOKED, false)) {
+ putLong(LAST_INIT_HOOK_TIME, System.currentTimeMillis())
+ }
+ if (intent.getBooleanExtra(Constants.INCREMENT_LAUNCH_COUNT, false)) {
+ val current = prefs.getLaunchedCount()
+ putInt(LAUNCHED_COUNT, current + 1)
+ }
+ }
+ }
+ }
+}
+
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 fb7a371..8d38fcd 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/SettingsActivity.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/SettingsActivity.kt
@@ -23,9 +23,10 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.core.animation.doOnEnd
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import ru.hepolise.volumekeytrackcontrol.ui.component.ModuleIsNotEnabled
import ru.hepolise.volumekeytrackcontrol.ui.navigation.AppNavigation
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.HOOK_PREFS_NAME
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SETTINGS_PREFS_NAME
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.isHooked
import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.getVibrator
import kotlin.system.exitProcess
@@ -44,14 +45,9 @@ class SettingsActivity : ComponentActivity() {
setUpSplashScreenAnimation()
enableEdgeToEdge()
setContent {
- val prefs = tryLoadPrefs(this)
-
+ val prefs = tryLoadPrefs()
MaterialTheme(colorScheme = dynamicColorScheme(context = this)) {
- if (prefs == null) {
- ModuleIsNotEnabled()
- } else {
- AppNavigation(sharedPreferences = prefs, vibrator = getVibrator())
- }
+ AppNavigation(settingsPrefs = prefs, vibrator = getVibrator())
}
}
}
@@ -63,10 +59,12 @@ class SettingsActivity : ComponentActivity() {
}
}
- private fun tryLoadPrefs(context: Context): SharedPreferences? = try {
+ private fun Context.tryLoadPrefs(): SharedPreferences? = try {
+ isHooked = getSharedPreferences(HOOK_PREFS_NAME, MODE_PRIVATE).isHooked()
+ shouldRemoveFromRecents = !isHooked
@SuppressLint("WorldReadableFiles")
@Suppress("DEPRECATION")
- context.getSharedPreferences(SETTINGS_PREFS_NAME, MODE_WORLD_READABLE)
+ getSharedPreferences(SETTINGS_PREFS_NAME, MODE_WORLD_READABLE)
} catch (_: SecurityException) {
shouldRemoveFromRecents = true
null
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/Util.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/Util.kt
index e6c3221..58f44dd 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/Util.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/Util.kt
@@ -20,4 +20,6 @@ fun State.debounce(
.collect { debouncedState.value = it }
}
return debouncedState
-}
\ No newline at end of file
+}
+
+var isHooked = false
\ No newline at end of file
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/AppFilterSetting.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/AppFilterSetting.kt
index 7da11ae..072e880 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/AppFilterSetting.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/AppFilterSetting.kt
@@ -1,35 +1,18 @@
package ru.hepolise.volumekeytrackcontrol.ui.component
import android.content.SharedPreferences
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.expandVertically
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.shrinkVertically
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
-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.MenuAnchorType
+import androidx.compose.material3.SegmentedButton
+import androidx.compose.material3.SegmentedButtonDefaults
+import androidx.compose.material3.SingleChoiceSegmentedButtonRow
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.sp
import androidx.core.content.edit
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.APP_FILTER_TYPE
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.AppFilterType
-import ru.hepolise.volumekeytrackcontrolmodule.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -37,64 +20,25 @@ fun AppFilterSetting(
value: AppFilterType,
sharedPreferences: SharedPreferences,
onValueChange: (AppFilterType) -> Unit,
- onNavigateToAppFilter: () -> Unit
) {
- Text(text = stringResource(R.string.app_filter), fontSize = 20.sp)
-
- var expanded by remember { mutableStateOf(false) }
-
- ExposedDropdownMenuBox(
- expanded = expanded,
- onExpandedChange = { expanded = !expanded }
+ SingleChoiceSegmentedButtonRow(
+ modifier = Modifier.fillMaxWidth()
) {
- TextField(
- value = stringResource(value.resourceId),
- onValueChange = {},
- readOnly = true,
- trailingIcon = {
- ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
- },
- modifier = Modifier
- .menuAnchor(MenuAnchorType.PrimaryNotEditable)
- )
- ExposedDropdownMenu(
- expanded = expanded,
- onDismissRequest = { expanded = false }) {
- AppFilterType.entries.forEach { type ->
- DropdownMenuItem(
- text = { Text(stringResource(type.resourceId)) },
- onClick = {
- onValueChange(type)
- sharedPreferences.edit {
- putString(APP_FILTER_TYPE, type.key)
- }
- expanded = false
+ AppFilterType.entries.forEachIndexed { index, type ->
+ SegmentedButton(
+ selected = value == type,
+ onClick = {
+ onValueChange(type)
+ sharedPreferences.edit {
+ putString(APP_FILTER_TYPE, type.key)
}
+ },
+ shape = SegmentedButtonDefaults.itemShape(
+ index = index,
+ count = AppFilterType.entries.size
)
- }
- }
- }
-
- Box {
- AnimatedVisibility(
- visible = value == AppFilterType.WHITE_LIST || value == AppFilterType.BLACK_LIST,
- enter = fadeIn() + expandVertically(expandFrom = Alignment.Top),
- exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Top),
- modifier = Modifier.fillMaxWidth()
- ) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
) {
- Button(
- onClick = onNavigateToAppFilter,
- ) {
- Text(
- text = stringResource(
- R.string.manage_apps,
- stringResource(value.resourceId)
- )
- )
- }
+ Text(text = stringResource(type.resourceId))
}
}
}
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
index a9a8fba..b757d0c 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/LongPressSetting.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/LongPressSetting.kt
@@ -3,12 +3,10 @@ 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
@@ -18,8 +16,6 @@ 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 androidx.core.content.edit
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LONG_PRESS_DURATION
import ru.hepolise.volumekeytrackcontrolmodule.R
@@ -30,19 +26,12 @@ fun LongPressSetting(
sharedPreferences: SharedPreferences,
onValueChange: (Int) -> Unit
) {
-
- Text(text = stringResource(R.string.long_press_settings), fontSize = 20.sp)
-
- Slider(
- value = longPressDuration.toFloat(),
- onValueChange = { onValueChange(it.toInt()) },
+ PrefsSlider(
+ value = longPressDuration,
+ onValueChange = { onValueChange(it) },
valueRange = 100f..1000f,
- onValueChangeFinished = {
- sharedPreferences.edit {
- putInt(LONG_PRESS_DURATION, longPressDuration)
- }
- },
- modifier = Modifier.widthIn(max = 300.dp)
+ prefKey = LONG_PRESS_DURATION,
+ sharedPreferences = sharedPreferences
)
var showLongPressTimeoutDialog by remember { mutableStateOf(false) }
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
deleted file mode 100644
index b72538f..0000000
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/ModuleIsNotEnabled.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-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)
- .padding(horizontal = 16.dp),
- 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.LSPOSED_GITHUB_URL,
- styles = TextLinkStyles(
- style = SpanStyle(
- color = MaterialTheme.colorScheme.primary,
- textDecoration = TextDecoration.Underline
- )
- )
- )
- ) {
- append(stringResource(id = R.string.recommended_lsposed_version))
- }
- }
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/PrefsSlider.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/PrefsSlider.kt
new file mode 100644
index 0000000..a3a2ae5
--- /dev/null
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/PrefsSlider.kt
@@ -0,0 +1,51 @@
+package ru.hepolise.volumekeytrackcontrol.ui.component
+
+import android.content.SharedPreferences
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.Slider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.core.content.edit
+import ru.hepolise.volumekeytrackcontrol.ui.debounce
+
+@Composable
+fun PrefsSlider(
+ value: Int,
+ valueRange: ClosedFloatingPointRange,
+ prefKey: String,
+ sharedPreferences: SharedPreferences,
+ modifier: Modifier = Modifier,
+ onValueChange: (Int) -> Unit,
+ debounceMillis: Long = 50,
+) {
+ val scope = rememberCoroutineScope()
+ var sliderValue by remember { mutableFloatStateOf(value.toFloat()) }
+
+ val debouncedValue by remember {
+ derivedStateOf { sliderValue.toInt() }
+ }.debounce(debounceMillis, scope)
+
+ LaunchedEffect(debouncedValue) {
+ sharedPreferences.edit {
+ putInt(prefKey, debouncedValue)
+ }
+ }
+
+ Slider(
+ value = value.toFloat(),
+ onValueChange = {
+ sliderValue = it
+ onValueChange(it.toInt())
+ },
+ valueRange = valueRange,
+ modifier = modifier.widthIn(max = 300.dp)
+ )
+}
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
index a9adc33..bfc931e 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/VibrationEffectSetting.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/component/VibrationEffectSetting.kt
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Button
@@ -24,7 +23,6 @@ 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
@@ -35,8 +33,6 @@ 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 androidx.core.content.edit
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.EFFECT
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_AMPLITUDE
@@ -80,7 +76,6 @@ fun VibrationEffectSetting(
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(
@@ -131,18 +126,12 @@ fun VibrationEffectSetting(
exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Top)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
- Slider(
- value = vibrationLength.toFloat(),
- onValueChange = {
- onValueChange(value.copy(vibrationLength = it.toInt()))
- },
+ PrefsSlider(
+ value = vibrationLength,
+ onValueChange = { onValueChange(value.copy(vibrationLength = it)) },
valueRange = 10f..500f,
- onValueChangeFinished = {
- sharedPreferences.edit {
- putInt(VIBRATION_LENGTH, vibrationLength)
- }
- },
- modifier = Modifier.widthIn(max = 300.dp)
+ prefKey = VIBRATION_LENGTH,
+ sharedPreferences = sharedPreferences
)
var showManualVibrationLengthDialog by remember { mutableStateOf(false) }
@@ -176,18 +165,12 @@ fun VibrationEffectSetting(
)
}
- Slider(
- value = vibrationAmplitude.toFloat(),
- onValueChange = {
- onValueChange(value.copy(vibrationAmplitude = it.toInt()))
- },
+ PrefsSlider(
+ value = vibrationAmplitude,
+ onValueChange = { onValueChange(value.copy(vibrationAmplitude = it)) },
valueRange = 1f..255f,
- onValueChangeFinished = {
- sharedPreferences.edit {
- putInt(VIBRATION_AMPLITUDE, vibrationAmplitude)
- }
- },
- modifier = Modifier.widthIn(max = 300.dp)
+ prefKey = VIBRATION_AMPLITUDE,
+ sharedPreferences = sharedPreferences
)
var showVibrationAmplitudeDialog by remember { mutableStateOf(false) }
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/navigation/AppNavigation.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/navigation/AppNavigation.kt
index 19def5a..335a86c 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/navigation/AppNavigation.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/navigation/AppNavigation.kt
@@ -17,7 +17,7 @@ import ru.hepolise.volumekeytrackcontrol.ui.screen.SettingsScreen
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil
@Composable
-fun AppNavigation(sharedPreferences: SharedPreferences, vibrator: Vibrator) {
+fun AppNavigation(settingsPrefs: SharedPreferences?, vibrator: Vibrator) {
val navController = rememberNavController()
NavHost(
@@ -28,40 +28,41 @@ fun AppNavigation(sharedPreferences: SharedPreferences, vibrator: Vibrator) {
route = "main",
) {
SettingsScreen(
- sharedPreferences = sharedPreferences,
+ settingsPrefs = settingsPrefs,
navController = navController,
vibrator = vibrator
)
}
- composable(
- route = "appFilter/{filterType}",
- enterTransition = {
- slideInHorizontally(
- initialOffsetX = { fullWidth -> fullWidth },
- animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
- ) + fadeIn(
- animationSpec = tween(durationMillis = 300)
+ settingsPrefs?.also { sharedPreferences ->
+ composable(
+ route = "appFilter/{filterType}",
+ enterTransition = {
+ slideInHorizontally(
+ initialOffsetX = { fullWidth -> fullWidth },
+ animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
+ ) + fadeIn(
+ animationSpec = tween(durationMillis = 300)
+ )
+ },
+ exitTransition = {
+ slideOutHorizontally(
+ targetOffsetX = { fullWidth -> fullWidth },
+ animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
+ ) + fadeOut(
+ animationSpec = tween(durationMillis = 300)
+ )
+ }
+ ) { backStackEntry ->
+ val filterType = SharedPreferencesUtil.AppFilterType.fromKey(
+ backStackEntry.arguments?.getString("filterType")
)
- },
- exitTransition = {
- slideOutHorizontally(
- targetOffsetX = { fullWidth -> fullWidth },
- animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
- ) + fadeOut(
- animationSpec = tween(durationMillis = 300)
+ AppFilterScreen(
+ filterType = filterType,
+ sharedPreferences = sharedPreferences,
+ navController = navController
)
}
- ) { backStackEntry ->
- val filterType = SharedPreferencesUtil.AppFilterType.fromKey(
- backStackEntry.arguments?.getString("filterType")
- ?: SharedPreferencesUtil.AppFilterType.DISABLED.key
- )
- AppFilterScreen(
- filterType = filterType,
- sharedPreferences = sharedPreferences,
- navController = navController
- )
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt
index 61a7979..1f91714 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt
@@ -3,28 +3,50 @@ package ru.hepolise.volumekeytrackcontrol.ui.screen
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
+import android.content.res.Configuration
import android.os.Vibrator
import android.widget.Toast
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Done
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Notifications
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.icons.filled.Star
+import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
-import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -33,11 +55,20 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString.Builder
+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.font.FontWeight
+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.content.edit
import androidx.core.net.toUri
import androidx.navigation.NavController
@@ -46,16 +77,19 @@ import ru.hepolise.volumekeytrackcontrol.ui.component.LongPressSetting
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.ui.isHooked
import ru.hepolise.volumekeytrackcontrol.util.Constants
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.APP_FILTER_TYPE_DEFAULT_VALUE
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.EFFECT_DEFAULT_VALUE
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.HOOK_PREFS_NAME
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.SETTINGS_PREFS_NAME
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_AMPLITUDE_DEFAULT_VALUE
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_LENGTH_DEFAULT_VALUE
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getAppFilterType
+import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLaunchedCount
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLongPressDuration
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getVibrationAmplitude
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getVibrationLength
@@ -68,94 +102,144 @@ import ru.hepolise.volumekeytrackcontrolmodule.R
@Composable
fun SettingsScreen(
navController: NavController?,
- sharedPreferences: SharedPreferences,
+ settingsPrefs: SharedPreferences?,
vibrator: Vibrator?
) {
val context = LocalContext.current
- var longPressDuration by remember { mutableIntStateOf(sharedPreferences.getLongPressDuration()) }
- 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()) }
- var appFilterType by remember { mutableStateOf(sharedPreferences.getAppFilterType()) }
+ val hookPrefs = context.getSharedPreferences(HOOK_PREFS_NAME, Context.MODE_PRIVATE)
+
+ var longPressDuration by remember { mutableIntStateOf(settingsPrefs.getLongPressDuration()) }
+ var vibrationType by remember { mutableStateOf(settingsPrefs.getVibrationType()) }
+ var vibrationLength by remember { mutableIntStateOf(settingsPrefs.getVibrationLength()) }
+ var vibrationAmplitude by remember { mutableIntStateOf(settingsPrefs.getVibrationAmplitude()) }
+ var isSwapButtons by remember { mutableStateOf(settingsPrefs.isSwapButtons()) }
+ var appFilterType by remember { mutableStateOf(settingsPrefs.getAppFilterType()) }
+ var showResetSettingsDialog by remember { mutableStateOf(false) }
+
+ val isHooked = isHooked.takeIf { settingsPrefs != null } ?: false
+ var launchedCount by remember { mutableIntStateOf(hookPrefs.getLaunchedCount()) }
+
+ val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
+ launchedCount = hookPrefs.getLaunchedCount()
+ }
+ hookPrefs.registerOnSharedPreferenceChangeListener(listener)
+
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
topBar = {
- TopAppBar(title = { Text(stringResource(R.string.app_name)) })
- }
+ LargeTopAppBar(
+ title = { Text(stringResource(R.string.app_name)) },
+ actions = {
+ if (settingsPrefs != null) {
+ IconButton(onClick = { showResetSettingsDialog = true }) {
+ Icon(
+ Icons.Default.Refresh,
+ contentDescription = stringResource(R.string.settings_reset)
+ )
+ }
+ }
+ IconButton(onClick = {
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = Constants.GITHUB_URL.toUri()
+ context.startActivity(intent)
+ }) {
+ Icon(
+ Icons.Default.Info,
+ contentDescription = stringResource(R.string.about)
+ )
+ }
+ },
+ scrollBehavior = scrollBehavior
+ )
+ },
) { padding ->
- Box(
+ Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
+ .nestedScroll(scrollBehavior.nestedScrollConnection)
+ .verticalScroll(rememberScrollState())
+ .padding(12.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
) {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(12.dp)
- .verticalScroll(rememberScrollState())
- .padding(bottom = 48.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally
+ SettingsCard(
+ icon = if (isHooked) Icons.Default.Done else Icons.Default.Warning,
+ title = stringResource(R.string.module_info),
) {
-
- LongPressSetting(longPressDuration, sharedPreferences) {
- longPressDuration = it
- }
-
- HorizontalDivider(modifier = Modifier.widthIn(max = 300.dp))
-
- VibrationEffectSetting(
- value = VibrationSettingData(
- vibrationType, vibrationLength, vibrationAmplitude
- ),
- vibrator = vibrator,
- sharedPreferences = sharedPreferences
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
) {
- vibrationType = it.vibrationType
- vibrationLength = it.vibrationLength
- vibrationAmplitude = it.vibrationAmplitude
+ Text(
+ stringResource(R.string.module_status),
+ style = MaterialTheme.typography.bodyMedium
+ )
+ Text(
+ text = stringResource(
+ if (isHooked) R.string.module_status_active
+ else R.string.module_status_inactive
+ ),
+ color = if (isHooked) MaterialTheme.colorScheme.primary
+ else MaterialTheme.colorScheme.error,
+ fontWeight = FontWeight.Bold
+ )
}
+ when {
+ isHooked -> ModuleIsEnabled(launchedCount)
+ settingsPrefs == null -> ModuleIsNotEnabled()
+ else -> ModuleInitError()
+ }
+ }
- HorizontalDivider(modifier = Modifier.widthIn(max = 300.dp))
-
- AppFilterSetting(
- value = appFilterType,
- sharedPreferences = sharedPreferences,
- onValueChange = { newAppFilterType -> appFilterType = newAppFilterType },
+ if (settingsPrefs != null && isHooked) {
+ SettingsCard(
+ icon = Icons.Default.Settings,
+ title = stringResource(R.string.long_press_settings)
) {
- navController?.navigate("appFilter/${appFilterType.key}")
+ LongPressSetting(longPressDuration, settingsPrefs) {
+ longPressDuration = it
+ }
+ SwapButtonsSetting(
+ isSwapButtons = isSwapButtons,
+ sharedPreferences = settingsPrefs
+ ) {
+ isSwapButtons = it
+ }
}
- HorizontalDivider(modifier = Modifier.widthIn(max = 300.dp))
-
- Text(text = stringResource(R.string.other_settings), fontSize = 20.sp)
-
- SwapButtonsSetting(
- isSwapButtons = isSwapButtons,
- sharedPreferences = sharedPreferences
+ SettingsCard(
+ icon = Icons.Default.Notifications,
+ title = stringResource(R.string.vibration_settings)
) {
- isSwapButtons = it
+ VibrationEffectSetting(
+ value = VibrationSettingData(
+ vibrationType,
+ vibrationLength,
+ vibrationAmplitude
+ ),
+ vibrator = vibrator,
+ sharedPreferences = settingsPrefs
+ ) {
+ vibrationType = it.vibrationType
+ vibrationLength = it.vibrationLength
+ vibrationAmplitude = it.vibrationAmplitude
+ }
}
- }
-
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .align(Alignment.BottomCenter)
- .padding(end = 16.dp, bottom = 8.dp)
- ) {
- Spacer(modifier = Modifier.weight(1f))
-
- var showResetSettingsDialog by remember { mutableStateOf(false) }
- Button(onClick = {
- showResetSettingsDialog = true
- }) {
- Text(stringResource(R.string.settings_reset))
+ SettingsCard(
+ icon = Icons.Default.Star,
+ title = stringResource(R.string.app_filter),
+ showAction = appFilterType != SharedPreferencesUtil.AppFilterType.DISABLED,
+ onActionClick = { navController?.navigate("appFilter/${appFilterType.key}") }
+ ) {
+ AppFilterSetting(
+ value = appFilterType,
+ sharedPreferences = settingsPrefs,
+ onValueChange = { appFilterType = it },
+ )
}
-
if (showResetSettingsDialog) {
AlertDialog(
onDismissRequest = { showResetSettingsDialog = false },
@@ -164,7 +248,7 @@ fun SettingsScreen(
confirmButton = {
Button(onClick = {
showResetSettingsDialog = false
- sharedPreferences.edit { clear() }
+ settingsPrefs.edit { clear() }
vibrationType = VibrationType.fromKey(EFFECT_DEFAULT_VALUE)
vibrationLength = VIBRATION_LENGTH_DEFAULT_VALUE
vibrationAmplitude = VIBRATION_AMPLITUDE_DEFAULT_VALUE
@@ -189,29 +273,141 @@ fun SettingsScreen(
}
)
}
+ }
+ }
+ }
+}
- Spacer(modifier = Modifier.width(8.dp))
-
- Button(onClick = {
- val intent = Intent(Intent.ACTION_VIEW)
- intent.data = Constants.GITHUB_URL.toUri()
- context.startActivity(intent)
- }) {
- Text(stringResource(R.string.about))
+@Composable
+fun SettingsCard(
+ icon: ImageVector,
+ title: String,
+ showAction: Boolean = false,
+ actionIcon: ImageVector? = null,
+ onActionClick: (() -> Unit)? = null,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ Card(
+ shape = RoundedCornerShape(24.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface,
+ contentColor = MaterialTheme.colorScheme.onSurface
+ ),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)),
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(icon, contentDescription = null)
+ Spacer(Modifier.width(12.dp))
+ Text(title, style = MaterialTheme.typography.titleMedium)
+ Spacer(Modifier.weight(1f))
+ AnimatedVisibility(
+ visible = showAction,
+ enter = fadeIn() + slideInHorizontally(initialOffsetX = { it / 2 }),
+ exit = fadeOut() + slideOutHorizontally(targetOffsetX = { it / 2 })
+ ) {
+ Icon(
+ imageVector = actionIcon ?: Icons.Default.Edit,
+ contentDescription = null,
+ modifier = Modifier.clickable(onClick = { onActionClick?.invoke() })
+ )
}
+ }
+ content()
+ }
+ }
+}
- Spacer(modifier = Modifier.width(8.dp))
+@Composable
+fun ModuleIsEnabled(launchedCount: Int) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ stringResource(R.string.module_launch_count),
+ style = MaterialTheme.typography.bodyMedium
+ )
+ Text(
+ text = launchedCount.toString(),
+ fontWeight = FontWeight.Medium
+ )
+ }
+}
+
+@Composable
+fun ModuleIsNotEnabled() {
+ Text(
+ text = buildAnnotatedString {
+ append(stringResource(id = R.string.module_is_not_enabled))
+ RecommendedLsposedVersion()
+ }
+ )
+}
+
+@Composable
+fun ModuleInitError() {
+ Text(
+ text = buildAnnotatedString {
+ append(stringResource(id = R.string.module_init_error))
+ append("\n")
+ append("\n")
+ withLink(
+ LinkAnnotation.Url(
+ url = Constants.GITHUB_NEW_ISSUE_URL,
+ styles = TextLinkStyles(
+ style = SpanStyle(
+ color = MaterialTheme.colorScheme.primary,
+ textDecoration = TextDecoration.Underline
+ )
+ )
+ )
+ ) {
+ append(stringResource(id = R.string.open_an_issue))
}
+ append(" ")
+ append(stringResource(id = R.string.if_the_problem_persists))
+ RecommendedLsposedVersion()
}
+ )
+}
+
+@Composable
+private fun Builder.RecommendedLsposedVersion() {
+ append("\n")
+ append("\n")
+ append(stringResource(id = R.string.recommended_lsposed_version))
+ append(" ")
+ withLink(
+ LinkAnnotation.Url(
+ url = Constants.LSPOSED_GITHUB_URL,
+ styles = TextLinkStyles(
+ style = SpanStyle(
+ color = MaterialTheme.colorScheme.primary,
+ textDecoration = TextDecoration.Underline
+ )
+ )
+ )
+ ) {
+ append(stringResource(id = R.string.recommended_lsposed_version_url))
}
}
-@Preview(showBackground = true)
+@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun PreviewSettingsScreen() {
SettingsScreen(
navController = null,
- sharedPreferences = LocalContext.current.getSharedPreferences(
+ settingsPrefs = LocalContext.current.getSharedPreferences(
SETTINGS_PREFS_NAME,
Context.MODE_PRIVATE
),
diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/Constants.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/Constants.kt
index 520e008..7079956 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/Constants.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/Constants.kt
@@ -1,7 +1,15 @@
package ru.hepolise.volumekeytrackcontrol.util
+import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig
+
object Constants {
const val GITHUB_URL = "https://github.com/Hepolise/VolumeKeyTrackControlModule"
+ const val GITHUB_NEW_ISSUE_URL =
+ "https://github.com/Hepolise/VolumeKeyTrackControlModule/issues/new/choose"
const val LSPOSED_GITHUB_URL =
"https://github.com/JingMatrix/LSPosed/actions/workflows/core.yml"
+
+ const val HOOK_UPDATE = BuildConfig.APPLICATION_ID + ".HOOK_UPDATE"
+ const val HOOKED = "hooked"
+ const val INCREMENT_LAUNCH_COUNT = "incrementLaunchCount"
}
\ 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 112ae3b..5596245 100644
--- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SharedPreferencesUtil.kt
+++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SharedPreferencesUtil.kt
@@ -2,6 +2,7 @@ package ru.hepolise.volumekeytrackcontrol.util
import android.content.SharedPreferences
import android.os.Build
+import android.os.SystemClock
import android.view.ViewConfiguration
import de.robv.android.xposed.XSharedPreferences
import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig
@@ -9,6 +10,7 @@ import ru.hepolise.volumekeytrackcontrolmodule.R
object SharedPreferencesUtil {
const val SETTINGS_PREFS_NAME = "settings_prefs"
+ const val HOOK_PREFS_NAME = "hook_prefs"
const val EFFECT = "selectedEffect"
const val VIBRATION_LENGTH = "vibrationLength"
@@ -19,6 +21,9 @@ object SharedPreferencesUtil {
const val WHITE_LIST_APPS = "whiteListApps"
const val BLACK_LIST_APPS = "blackListApps"
+ const val LAST_INIT_HOOK_TIME = "lastInitHookTime"
+ const val LAUNCHED_COUNT = "launchedCount"
+
val 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
@@ -27,6 +32,8 @@ object SharedPreferencesUtil {
const val IS_SWAP_BUTTONS_DEFAULT_VALUE = false
val APP_FILTER_TYPE_DEFAULT_VALUE = AppFilterType.DISABLED.key
+ const val LAUNCHED_COUNT_DEFAULT_VALUE = 0
+
fun SharedPreferences?.getVibrationType(): VibrationType {
val defaultValue = EFFECT_DEFAULT_VALUE
return VibrationType.fromKey(this?.getString(EFFECT, defaultValue) ?: defaultValue)
@@ -68,11 +75,21 @@ object SharedPreferencesUtil {
}
}
+ fun SharedPreferences.isHooked(): Boolean =
+ getLong(
+ LAST_INIT_HOOK_TIME,
+ 0L
+ ) >= (System.currentTimeMillis() - SystemClock.elapsedRealtime())
- fun prefs(): SharedPreferences? {
- val pref = XSharedPreferences(BuildConfig.APPLICATION_ID, SETTINGS_PREFS_NAME)
- return if (pref.file.canRead()) pref else null
- }
+ fun SharedPreferences.getLaunchedCount(): Int =
+ this.getInt(LAUNCHED_COUNT, LAUNCHED_COUNT_DEFAULT_VALUE)
+
+ private var _prefs: SharedPreferences? = null
+
+ fun prefs(): SharedPreferences? =
+ XSharedPreferences(BuildConfig.APPLICATION_ID, SETTINGS_PREFS_NAME)
+ .takeIf { it.file.canRead() }
+ ?.also { _prefs = it } ?: _prefs
enum class AppFilterType(
val value: Int,
@@ -84,7 +101,7 @@ object SharedPreferencesUtil {
BLACK_LIST(2, "blacklist", R.string.app_filter_black_list);
companion object {
- fun fromKey(key: String) = entries.find { it.key == key } ?: DISABLED
+ fun fromKey(key: String?) = entries.find { it.key == key } ?: DISABLED
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e970697..2c82e94 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,6 +1,12 @@
Volume Key Track Control
+ Module Info
+ Status:
+ Active
+ Inactive
+ Launch Count:
+
Vibration Settings
Long Press Settings
Other Settings
@@ -50,8 +56,12 @@
Settings are reset to default
About
- Module is not enabled in LSPosed.\nIf the module is enabled and you still see this screen, try to restart the app.\nRecommended LSPosed version:
- JingMatrix
+ Module is not enabled in LSPosed.\nIf the module is enabled and you still see this screen, try to restart the app.
+ Module initialization error.\nIf the module is enabled and you still see this screen, check if you have selected recommended apps and rebooted your device.
+ Open an issue
+ if the problem persists.
+ Recommended LSPosed version:
+ JingMatrix
Back
diff --git a/build.gradle.kts b/build.gradle.kts
index a61b71a..dd98523 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,8 +1,8 @@
plugins {
- id("com.android.application") version "8.12.0" apply false
- id("com.android.library") version "8.12.0" apply false
- id("org.jetbrains.kotlin.android") version "2.2.10" apply false
- id("org.jetbrains.kotlin.plugin.compose") version "2.2.10" apply false
+ id("com.android.application") version "8.13.0" apply false
+ id("com.android.library") version "8.13.0" apply false
+ id("org.jetbrains.kotlin.android") version "2.2.20" apply false
+ id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" apply false
}
val versionName = "1.15.7"
diff --git a/settings.gradle b/settings.gradle.kts
similarity index 83%
rename from settings.gradle
rename to settings.gradle.kts
index df7c36c..8cd08e7 100644
--- a/settings.gradle
+++ b/settings.gradle.kts
@@ -5,15 +5,17 @@ pluginManagement {
mavenCentral()
}
}
+
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven {
- url "https://api.xposed.info/"
+ url = uri("https://api.xposed.info/")
}
google()
mavenCentral()
}
}
+
rootProject.name = "VolumeKeyTrackControlModule"
-include ':app'
+include(":app")