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")