Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ dependencies {
implementation("androidx.compose.material3:material3:1.3.2")
implementation("androidx.core:core-splashscreen:1.0.1")

// Compose navigation
implementation("androidx.navigation:navigation-compose:2.8.9")

// Coil
implementation("io.coil-kt:coil-compose:2.6.0")

// Required for preview support
debugImplementation("androidx.compose.ui:ui-tooling")

Expand Down
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />

<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ 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.util.SharedPreferencesUtil
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getAppFilterType
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getApps
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLongPressDuration
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.isSwapButtons
import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.getVibrator
Expand Down Expand Up @@ -72,7 +74,7 @@ object VolumeKeyControlModuleHandlers {
if (isUpPressed && isDownPressed) {
log("sending play/pause")
isLongPress = true
getActiveMediaController()?.transportControls?.also {
getMediaController()?.transportControls?.also {
sendMediaButtonEventAndTriggerVibration(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)
}
} else {
Expand Down Expand Up @@ -178,7 +180,7 @@ object VolumeKeyControlModuleHandlers {
handleVolumeSkipPressAbort(param.thisObject)
} else {
// only one button pressed
if (isMusicActive()) {
if (getMediaController()?.isMusicActive() == true) {
log("music is active, creating delayed skip")
handleVolumeSkipPress(param.thisObject, isDown)
}
Expand All @@ -195,7 +197,7 @@ object VolumeKeyControlModuleHandlers {
}
log("up action received, down: $isDownPressed, up: $isUpPressed")
handleVolumeAllPressAbort(param.thisObject)
if (!isLongPress && isMusicActive()) {
if (!isLongPress && getMediaController()?.isMusicActive() == true) {
log("adjusting music volume")
val direction = when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> AudioManager.ADJUST_RAISE
Expand All @@ -206,27 +208,47 @@ object VolumeKeyControlModuleHandlers {
}
}

private fun hasActiveMediaController() = getActiveMediaController() != null
private fun hasActiveMediaController(): Boolean {
val first = getFirstMediaController()
val active = getMediaController()
if (first != active && first != null) {
return !first.isMusicActive()
}
return active != null
}

private fun getActiveMediaController(): MediaController? {
return mediaControllers?.firstOrNull()?.also { log("chosen media controller: ${it.packageName}") }
private fun getFirstMediaController(): MediaController? {
return mediaControllers?.firstOrNull()
?.also { log("first media controller: ${it.packageName}") }
}

private fun isMusicActive() = getActiveMediaController()?.let {
when (it.playbackState?.state) {
PlaybackState.STATE_PLAYING,
PlaybackState.STATE_FAST_FORWARDING,
PlaybackState.STATE_REWINDING,
PlaybackState.STATE_BUFFERING -> true
else -> false
}
} ?: false
private fun getMediaController(): MediaController? {
return mediaControllers?.find {
val filterType = SharedPreferencesUtil.prefs().getAppFilterType()
val apps = SharedPreferencesUtil.prefs().getApps(filterType)
when (filterType) {
SharedPreferencesUtil.AppFilterType.Disabled -> true
SharedPreferencesUtil.AppFilterType.WhiteList -> it.packageName in apps
SharedPreferencesUtil.AppFilterType.BlackList -> it.packageName !in apps
}
}?.also { log("chosen media controller: ${it.packageName}") }
}

private fun MediaController.isMusicActive() = when (playbackState?.state) {
PlaybackState.STATE_PLAYING,
PlaybackState.STATE_FAST_FORWARDING,
PlaybackState.STATE_REWINDING,
PlaybackState.STATE_BUFFERING -> true

else -> false
}

private fun sendMediaButtonEventAndTriggerVibration(keyCode: Int) {
getActiveMediaController()?.transportControls?.also { controls ->
getMediaController()?.also { controller ->
val controls = controller.transportControls
when (keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
if (isMusicActive()) controls.pause() else controls.play()
if (controller.isMusicActive()) controls.pause() else controls.play()
}

KeyEvent.KEYCODE_MEDIA_NEXT -> controls.skipToNext()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,80 +4,29 @@ import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Vibrator
import android.view.View
import android.view.animation.AccelerateInterpolator
import android.view.animation.AnticipateInterpolator
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
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.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.animation.doOnEnd
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import ru.hepolise.volumekeytrackcontrol.ui.component.LongPressSetting
import ru.hepolise.volumekeytrackcontrol.ui.component.ModuleIsNotEnabled
import ru.hepolise.volumekeytrackcontrol.ui.component.SwapButtonsSetting
import ru.hepolise.volumekeytrackcontrol.ui.component.VibrationEffectSetting
import ru.hepolise.volumekeytrackcontrol.ui.component.VibrationSettingData
import ru.hepolise.volumekeytrackcontrol.util.Constants
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.IS_SWAP_BUTTONS_DEFAULT_VALUE
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LONG_PRESS_DURATION_DEFAULT_VALUE
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SELECTED_EFFECT_DEFAULT_VALUE
import ru.hepolise.volumekeytrackcontrol.ui.navigation.AppNavigation
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.getLongPressDuration
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getVibrationAmplitude
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getVibrationLength
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getVibrationType
import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.isSwapButtons
import ru.hepolise.volumekeytrackcontrol.util.VibrationType
import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.getVibrator
import ru.hepolise.volumekeytrackcontrolmodule.R


class SettingsActivity : ComponentActivity() {
Expand Down Expand Up @@ -115,141 +64,16 @@ class SettingsActivity : ComponentActivity() {
MaterialTheme(
colorScheme = dynamicColorScheme(context = this)
) {
VibrationSettingsScreen(vibrator = getVibrator())
}
}
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VibrationSettingsScreen(vibrator: Vibrator?) {
val context = LocalContext.current

@SuppressLint("WorldReadableFiles") @Suppress("DEPRECATION")
val sharedPreferences = try {
context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_WORLD_READABLE)
} catch (e: SecurityException) {
ModuleIsNotEnabled()
return
}

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()) }

Scaffold(
topBar = {
TopAppBar(title = { Text(stringResource(R.string.app_name)) })
}
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(12.dp)
.verticalScroll(rememberScrollState())
.padding(bottom = 48.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {

LongPressSetting(longPressDuration, sharedPreferences) {
longPressDuration = it
val context = LocalContext.current

@SuppressLint("WorldReadableFiles") @Suppress("DEPRECATION")
val sharedPreferences = try {
context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_WORLD_READABLE)
} catch (_: SecurityException) {
ModuleIsNotEnabled()
return@MaterialTheme
}

HorizontalDivider(modifier = Modifier.widthIn(max = 300.dp))

VibrationEffectSetting(
value = VibrationSettingData(
vibrationType, vibrationLength, vibrationAmplitude
),
vibrator = vibrator,
sharedPreferences = sharedPreferences
) {
vibrationType = it.vibrationType
vibrationLength = it.vibrationLength
vibrationAmplitude = it.vibrationAmplitude
}



HorizontalDivider(modifier = Modifier.widthIn(max = 300.dp))

Text(text = stringResource(R.string.other_settings), fontSize = 20.sp)

SwapButtonsSetting(
isSwapButtons = isSwapButtons,
sharedPreferences = sharedPreferences
) {
isSwapButtons = it
}

}

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

if (showResetSettingsDialog) {
AlertDialog(
onDismissRequest = { showResetSettingsDialog = false },
title = { Text(stringResource(R.string.settings_reset)) },
text = { Text(stringResource(R.string.settings_reset_message)) },
confirmButton = {
Button(onClick = {
showResetSettingsDialog = false
sharedPreferences.edit().clear().apply()
vibrationType = VibrationType.fromKey(SELECTED_EFFECT_DEFAULT_VALUE)
vibrationLength = VIBRATION_LENGTH_DEFAULT_VALUE
vibrationAmplitude = VIBRATION_AMPLITUDE_DEFAULT_VALUE
longPressDuration = LONG_PRESS_DURATION_DEFAULT_VALUE
isSwapButtons = IS_SWAP_BUTTONS_DEFAULT_VALUE
Toast.makeText(
context,
context.getString(R.string.settings_reset_toast),
Toast.LENGTH_SHORT
).show()
}) {
Text(stringResource(R.string.yes))
}
},
dismissButton = {
TextButton(onClick = { showResetSettingsDialog = false }) {
Text(stringResource(R.string.no))
}
}
)
}

Spacer(modifier = Modifier.width(8.dp))

Button(onClick = {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(Constants.GITHUB_URL)
context.startActivity(intent)
}) {
Text(stringResource(R.string.about))
}

Spacer(modifier = Modifier.width(8.dp))
AppNavigation(sharedPreferences = sharedPreferences, vibrator = getVibrator())
}
}
}
Expand All @@ -265,9 +89,3 @@ fun dynamicColorScheme(context: Context): ColorScheme {
if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
}
}

@Preview(showBackground = true)
@Composable
fun PreviewVibrationSettingsScreen() {
VibrationSettingsScreen(null)
}
23 changes: 23 additions & 0 deletions app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/Util.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ru.hepolise.volumekeytrackcontrol.ui

import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch

@OptIn(FlowPreview::class)
fun <T> State<T>.debounce(
durationMillis: Long,
coroutineScope: CoroutineScope
): State<T> {
val debouncedState = mutableStateOf(this.value)
coroutineScope.launch {
snapshotFlow { this@debounce.value }
.debounce(durationMillis)
.collect { debouncedState.value = it }
}
return debouncedState
}
Loading