diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ff5c61..bb1cece 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,25 +14,39 @@ jobs: runs-on: ubuntu-latest needs: call-build steps: + - name: Checkout Code + uses: actions/checkout@v4 + - name: Download Build Artifacts uses: actions/download-artifact@v4 with: name: Build Artifacts - - name: Generate Dynamic Tag - id: tag - run: echo "TAG_NAME=$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_ENV + - name: Extract APK Version + id: apk_version + run: | + APK_FILE=$(ls *.apk | head -n 1) # Get the first APK file + echo "Found APK: $APK_FILE" + VERSION=$(echo $APK_FILE | grep -oP '\d+\.\d+\.\d+') + echo "Extracted Version: $VERSION" + echo "VERSION=$VERSION" >> $GITHUB_ENV + + - name: Generate Changelog + id: changelog + run: | + echo "CHANGELOG<> $GITHUB_ENV + git log --pretty=format:"- %s [%h]" $(git describe --tags --abbrev=0)..HEAD >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV - name: Create Release id: create_release uses: actions/create-release@v1 with: - tag_name: ${{ env.TAG_NAME }} - release_name: Release ${{ env.TAG_NAME }} - body: | - Automatically generated release. - draft: false - prerelease: false + tag_name: v${{ env.VERSION }} + release_name: Release v${{ env.VERSION }} + body: ${{ env.CHANGELOG }} + draft: true + prerelease: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index ad6a686..2c06da0 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ Works when the screen is off. ## Requirements -Android 11+ (may be working on lower versions, you can try and install it even on Android 7) +* [LSPosed](https://github.com/LSPosed/LSPosed) 93+ +* Android 11+ (may be working on lower versions, you can try and install it even on Android 8.1) ## Tested on diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 41bc54a..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,39 +0,0 @@ -plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' -} - -android { - compileSdk 35 - - defaultConfig { - applicationId "ru.hepolise.volumekeymusicmanagermodule" - minSdk 25 - targetSdk 35 - versionCode = Integer.parseInt(rootProject.ext["appVersionCode"].toString()) - versionName = rootProject.ext["appVersionName"].toString() - } - - buildTypes { - release { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = '17' - } - namespace 'ru.hepolise.volumekeytrackcontrolmodule' -} - -dependencies { - implementation(platform("org.jetbrains.kotlin:kotlin-bom:2.1.0")) - implementation 'androidx.core:core-ktx:1.15.0' - - // Xposed Framework API dependencies - compileOnly 'de.robv.android.xposed:api:82' -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..35cf157 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") +} + +android { + compileSdk = 35 + + defaultConfig { + applicationId = "ru.hepolise.volumekeymusicmanagermodule" + minSdk = 27 + targetSdk = 35 + versionCode = rootProject.ext["appVersionCode"].toString().toInt() + versionName = rootProject.ext["appVersionName"].toString() + } + + buildTypes { + getByName("release") { + isMinifyEnabled = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + namespace = "ru.hepolise.volumekeytrackcontrolmodule" +} + +dependencies { + implementation(platform("org.jetbrains.kotlin:kotlin-bom:2.1.0")) + implementation("androidx.core:core-ktx:1.15.0") + + // Compose BOM (Bill of Materials) + implementation(platform("androidx.compose:compose-bom:2024.12.01")) + + // Compose dependencies + implementation("androidx.activity:activity-compose:1.9.3") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3:1.3.1") + + // Required for preview support + debugImplementation("androidx.compose.ui:ui-tooling") + + // Xposed Framework API dependencies + compileOnly("de.robv.android.xposed:api:82") +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2b7248a..1d08be0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,11 +1,23 @@ + + android:theme="@android:style/Theme.Material.NoActionBar"> + + + + + + + + + android:value="93" /> @@ -22,6 +34,4 @@ android:resource="@array/module_scope" /> - - diff --git a/app/src/main/assets/xposed_init b/app/src/main/assets/xposed_init index aafec20..298b3f2 100644 --- a/app/src/main/assets/xposed_init +++ b/app/src/main/assets/xposed_init @@ -1 +1 @@ -ru.hepolise.volumekeytrackcontrolmodule.VolumeControlModule +ru.hepolise.volumekeytrackcontrol.module.VolumeControlModule diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrolmodule/VolumeControlModule.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeControlModule.kt similarity index 91% rename from app/src/main/java/ru/hepolise/volumekeytrackcontrolmodule/VolumeControlModule.kt rename to app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeControlModule.kt index 6c5758d..f6deafa 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrolmodule/VolumeControlModule.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeControlModule.kt @@ -1,4 +1,4 @@ -package ru.hepolise.volumekeytrackcontrolmodule +package ru.hepolise.volumekeytrackcontrol.module import android.content.Context import android.view.KeyEvent @@ -6,8 +6,9 @@ import androidx.annotation.Keep import de.robv.android.xposed.IXposedHookLoadPackage import de.robv.android.xposed.XposedHelpers import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam -import ru.hepolise.volumekeytrackcontrolmodule.VolumeKeyControlModuleHandlers.handleConstructPhoneWindowManager -import ru.hepolise.volumekeytrackcontrolmodule.VolumeKeyControlModuleHandlers.handleInterceptKeyBeforeQueueing +import ru.hepolise.volumekeytrackcontrol.module.VolumeKeyControlModuleHandlers.handleConstructPhoneWindowManager +import ru.hepolise.volumekeytrackcontrol.module.VolumeKeyControlModuleHandlers.handleInterceptKeyBeforeQueueing +import ru.hepolise.volumekeytrackcontrol.module.util.LogHelper import java.io.Serializable @Keep diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrolmodule/VolumeKeyControlModuleHandlers.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt similarity index 93% rename from app/src/main/java/ru/hepolise/volumekeytrackcontrolmodule/VolumeKeyControlModuleHandlers.kt rename to app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt index 6b04955..46b717c 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrolmodule/VolumeKeyControlModuleHandlers.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/VolumeKeyControlModuleHandlers.kt @@ -1,22 +1,23 @@ -package ru.hepolise.volumekeytrackcontrolmodule +package ru.hepolise.volumekeytrackcontrol.module import android.content.Context import android.hardware.display.DisplayManager import android.media.AudioManager import android.media.session.MediaController import android.media.session.PlaybackState -import android.os.Build import android.os.Handler import android.os.PowerManager import android.os.Vibrator -import android.os.VibratorManager import android.view.Display import android.view.KeyEvent -import android.view.ViewConfiguration import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XC_MethodHook.MethodHookParam import de.robv.android.xposed.XposedHelpers -import ru.hepolise.volumekeytrackcontrolmodule.extension.VibratorExtension.triggerVibration +import ru.hepolise.volumekeytrackcontrol.module.util.LogHelper +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLongPressDuration +import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.getVibrator +import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.triggerVibration object VolumeKeyControlModuleHandlers { @@ -29,8 +30,6 @@ object VolumeKeyControlModuleHandlers { "android.media.session.MediaSessionLegacyHelper" private const val CLASS_COMPONENT_NAME = "android.content.ComponentName" - private val TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong() - private var isLongPress = false private var isDownPressed = false private var isUpPressed = false @@ -129,14 +128,7 @@ object VolumeKeyControlModuleHandlers { ?: throw NullPointerException("Unable to obtain power service") displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager? ?: throw NullPointerException("Unable to obtain display service") - vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val vibratorManager = - getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager - vibratorManager.defaultVibrator - } else { - @Suppress("DEPRECATION") - getSystemService(Context.VIBRATOR_SERVICE) as Vibrator - } + vibrator = context.getVibrator() } val mediaSessionHelperClass = XposedHelpers.findClass( @@ -237,7 +229,10 @@ object VolumeKeyControlModuleHandlers { private fun handleVolumePlayPausePress(instance: Any) { val handler = instance.getHandler() - handler.postDelayed(getRunnable(instance, VOLUME_BOTH_LONG_PRESS), TIMEOUT) + handler.postDelayed( + getRunnable(instance, VOLUME_BOTH_LONG_PRESS), + SharedPreferencesUtil.prefs().getLongPressDuration() + ) } private fun handleVolumeSkipPress(instance: Any, keyCode: Int) { @@ -248,7 +243,7 @@ object VolumeKeyControlModuleHandlers { KeyEvent.KEYCODE_VOLUME_DOWN -> getRunnable(instance, VOLUME_DOWN_LONG_PRESS) else -> return }, - TIMEOUT + SharedPreferencesUtil.prefs().getLongPressDuration() ) } diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrolmodule/LogHelper.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/LogHelper.kt similarity index 61% rename from app/src/main/java/ru/hepolise/volumekeytrackcontrolmodule/LogHelper.kt rename to app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/LogHelper.kt index ababd45..84c443b 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrolmodule/LogHelper.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/module/util/LogHelper.kt @@ -1,6 +1,7 @@ -package ru.hepolise.volumekeytrackcontrolmodule +package ru.hepolise.volumekeytrackcontrol.module.util import de.robv.android.xposed.XposedBridge +import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig object LogHelper { fun log(prefix: String, text: String) { diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/SettingsActivity.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/SettingsActivity.kt new file mode 100644 index 0000000..6747bf3 --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/SettingsActivity.kt @@ -0,0 +1,309 @@ +package ru.hepolise.volumekeytrackcontrol.ui + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.VibrationEffect +import android.os.Vibrator +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.clickable +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.height +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.Button +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf +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 ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LONG_PRESS_DURATION +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.LONG_PRESS_DURATION_DEFAULT_VALUE +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SELECTED_EFFECT +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SELECTED_EFFECT_DEFAULT_VALUE +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.SETTINGS_PREFS_NAME +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_LENGTH +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_LENGTH_DEFAULT_VALUE +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_MODE +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.VIBRATION_PREDEFINED_MODE_DEFAULT_VALUE +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getLongPressDuration +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getSelectedEffect +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getVibrationLength +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.isVibrationModePredefined +import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.PredefinedEffects +import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.getVibrator +import ru.hepolise.volumekeytrackcontrol.util.VibratorUtil.triggerVibration +import ru.hepolise.volumekeytrackcontrolmodule.R +import kotlin.system.exitProcess + +class SettingsActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + MaterialTheme( + colorScheme = dynamicColorScheme(context = this) + ) { + VibrationSettingsScreen(vibrator = getVibrator()) + } + } + } +} + +private val VibrationEffectTitles = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + mapOf( + VibrationEffect.EFFECT_CLICK to R.string.effect_click, + VibrationEffect.EFFECT_DOUBLE_CLICK to R.string.effect_double_click, + VibrationEffect.EFFECT_HEAVY_CLICK to R.string.effect_heavy_click, + VibrationEffect.EFFECT_TICK to R.string.effect_tick + ) +} else emptyMap() + +@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) { + Toast.makeText(context, R.string.module_is_not_enabled, Toast.LENGTH_LONG).show() + // Clear the application stack and exit + (context as? Activity)?.finishAffinity() + exitProcess(0) + } + + var isVibrationModePredefined by remember { mutableStateOf(sharedPreferences.isVibrationModePredefined()) } + var selectedEffect by remember { mutableIntStateOf(sharedPreferences.getSelectedEffect()) } + var vibrationLength by remember { mutableLongStateOf(sharedPreferences.getVibrationLength()) } + var longPressDuration by remember { mutableLongStateOf(sharedPreferences.getLongPressDuration()) } + + Scaffold( + topBar = { + TopAppBar(title = { Text(stringResource(R.string.app_name)) }) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text(text = stringResource(R.string.long_press_settings), fontSize = 20.sp) + + Slider( + value = longPressDuration.toFloat(), + onValueChange = { + longPressDuration = it.toLong() + }, + valueRange = 100f..1000f, + onValueChangeFinished = { + sharedPreferences.edit().putLong(LONG_PRESS_DURATION, longPressDuration).apply() + }, + modifier = Modifier.widthIn(max = 300.dp) + ) + Text(stringResource(R.string.long_press_duration, longPressDuration)) + + Spacer(modifier = Modifier.height(20.dp)) + + Text(text = stringResource(R.string.vibration_settings), fontSize = 20.sp) + + Column( + modifier = Modifier + .fillMaxWidth() + .height(116.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier.widthIn(min = 150.dp), + contentAlignment = Alignment.CenterStart + ) { + Text( + text = if (isVibrationModePredefined) { + stringResource(id = R.string.predefined_vibration) + } else { + stringResource(id = R.string.manual_vibration) + }, + modifier = Modifier + .padding(end = 8.dp) + .clickable { + isVibrationModePredefined = !isVibrationModePredefined + sharedPreferences + .edit() + .putBoolean(VIBRATION_MODE, isVibrationModePredefined) + .apply() + } + ) + } + Switch( + checked = isVibrationModePredefined, + onCheckedChange = { + isVibrationModePredefined = it + sharedPreferences.edit().putBoolean(VIBRATION_MODE, it).apply() + } + ) + } + } + + if (isVibrationModePredefined && PredefinedEffects.isNotEmpty()) { + var effectExpanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = effectExpanded, + onExpandedChange = { effectExpanded = !effectExpanded } + ) { + TextField( + value = stringResource(VibrationEffectTitles[PredefinedEffects[selectedEffect]]!!), + onValueChange = {}, + readOnly = true, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = effectExpanded) + }, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable) + ) + ExposedDropdownMenu( + expanded = effectExpanded, + onDismissRequest = { effectExpanded = false }) { + PredefinedEffects.forEachIndexed { index, effect -> + DropdownMenuItem( + text = { Text(stringResource(VibrationEffectTitles[effect]!!)) }, + onClick = { + selectedEffect = index + sharedPreferences.edit().putInt(SELECTED_EFFECT, index) + .apply() + effectExpanded = false + } + ) + } + } + } + } else if (!isVibrationModePredefined) { + Slider( + value = vibrationLength.toFloat(), + onValueChange = { + vibrationLength = it.toLong() + }, + valueRange = 10f..500f, + onValueChangeFinished = { + sharedPreferences.edit().putLong(VIBRATION_LENGTH, vibrationLength) + .apply() + }, + modifier = Modifier.widthIn(max = 300.dp) + ) + Text(stringResource(R.string.vibration_length, vibrationLength)) + } + } + + Button(onClick = { + vibrator?.triggerVibration(sharedPreferences) + }) { + Text(stringResource(R.string.test_vibration)) + } + + Spacer(modifier = Modifier.weight(1f)) + + Row(modifier = Modifier.fillMaxWidth()) { + + Spacer(modifier = Modifier.weight(1f)) + + Button(onClick = { + sharedPreferences.edit().clear().apply() + isVibrationModePredefined = VIBRATION_PREDEFINED_MODE_DEFAULT_VALUE + selectedEffect = SELECTED_EFFECT_DEFAULT_VALUE + vibrationLength = VIBRATION_LENGTH_DEFAULT_VALUE + longPressDuration = LONG_PRESS_DURATION_DEFAULT_VALUE + Toast.makeText( + context, + context.getString(R.string.settings_reset), + Toast.LENGTH_SHORT + ).show() + }) { + Text(stringResource(R.string.restore_default)) + } + + Spacer(modifier = Modifier.width(8.dp)) + + Button(onClick = { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = + Uri.parse("https://github.com/Hepolise/VolumeKeyTrackControlModule") + context.startActivity(intent) + }) { + Text(stringResource(R.string.about)) + } + + Spacer(modifier = Modifier.width(8.dp)) + } + } + } +} + +@Composable +fun dynamicColorScheme(context: Context): ColorScheme { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Dynamic colors for Android 12+ + if (isSystemInDarkTheme()) dynamicDarkColorScheme(context) + else dynamicLightColorScheme(context) + } else { + if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewVibrationSettingsScreen() { + VibrationSettingsScreen(null) +} diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SharedPreferencesUtil.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SharedPreferencesUtil.kt new file mode 100644 index 0000000..6933c18 --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/SharedPreferencesUtil.kt @@ -0,0 +1,48 @@ +package ru.hepolise.volumekeytrackcontrol.util + +import android.content.SharedPreferences +import android.os.Build +import android.view.ViewConfiguration +import de.robv.android.xposed.XSharedPreferences +import ru.hepolise.volumekeytrackcontrolmodule.BuildConfig + +object SharedPreferencesUtil { + const val SETTINGS_PREFS_NAME = "settings_prefs" + + const val VIBRATION_MODE = "vibrationMode" + const val SELECTED_EFFECT = "selectedEffect" + const val VIBRATION_LENGTH = "vibrationLength" + const val LONG_PRESS_DURATION = "longPressDuration" + + val VIBRATION_PREDEFINED_MODE_DEFAULT_VALUE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + const val SELECTED_EFFECT_DEFAULT_VALUE = 0 + const val VIBRATION_LENGTH_DEFAULT_VALUE = 50L + val LONG_PRESS_DURATION_DEFAULT_VALUE = ViewConfiguration.getLongPressTimeout().toLong() + + fun SharedPreferences?.isVibrationModePredefined(): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return false + val defaultValue = VIBRATION_PREDEFINED_MODE_DEFAULT_VALUE + return this?.getBoolean(VIBRATION_MODE, defaultValue) ?: defaultValue + } + + fun SharedPreferences?.getSelectedEffect(): Int { + val defaultValue = SELECTED_EFFECT_DEFAULT_VALUE + return this?.getInt(SELECTED_EFFECT, defaultValue) ?: defaultValue + } + + fun SharedPreferences?.getVibrationLength(): Long { + val defaultValue = VIBRATION_LENGTH_DEFAULT_VALUE + return this?.getLong(VIBRATION_LENGTH, defaultValue) ?: defaultValue + } + + fun SharedPreferences?.getLongPressDuration(): Long { + val defaultValue = LONG_PRESS_DURATION_DEFAULT_VALUE + return this?.getLong(LONG_PRESS_DURATION, defaultValue) ?: defaultValue + } + + fun prefs(): SharedPreferences? { + val pref = XSharedPreferences(BuildConfig.APPLICATION_ID, SETTINGS_PREFS_NAME) + return if (pref.file.canRead()) pref else null + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/VibratorUtil.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/VibratorUtil.kt new file mode 100644 index 0000000..453da92 --- /dev/null +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/util/VibratorUtil.kt @@ -0,0 +1,51 @@ +package ru.hepolise.volumekeytrackcontrol.util + +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getSelectedEffect +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.getVibrationLength +import ru.hepolise.volumekeytrackcontrol.util.SharedPreferencesUtil.isVibrationModePredefined + +object VibratorUtil { + + val PredefinedEffects = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + listOf( + VibrationEffect.EFFECT_CLICK, + VibrationEffect.EFFECT_DOUBLE_CLICK, + VibrationEffect.EFFECT_HEAVY_CLICK, + VibrationEffect.EFFECT_TICK + ) + } else emptyList() + + fun Context.getVibrator(): Vibrator { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = + getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibratorManager.defaultVibrator + } else { + @Suppress("DEPRECATION") + getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + } + } + + fun Vibrator.triggerVibration(prefs: SharedPreferences? = SharedPreferencesUtil.prefs()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && prefs.isVibrationModePredefined()) { + this.vibrate( + VibrationEffect.createPredefined(PredefinedEffects[prefs.getSelectedEffect()]) + ) + } else { + val millis = prefs.getVibrationLength() + this.vibrate( + VibrationEffect.createOneShot( + millis, + VibrationEffect.DEFAULT_AMPLITUDE + ) + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrolmodule/extension/VibratorExtension.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrolmodule/extension/VibratorExtension.kt deleted file mode 100644 index e760438..0000000 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrolmodule/extension/VibratorExtension.kt +++ /dev/null @@ -1,30 +0,0 @@ -package ru.hepolise.volumekeytrackcontrolmodule.extension - -import android.annotation.SuppressLint -import android.os.Build -import android.os.VibrationEffect -import android.os.Vibrator - -object VibratorExtension { - - @SuppressLint("MissingPermission") - fun Vibrator.triggerVibration() { - val millis = 50L - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - this.vibrate( - VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK) - ) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - this.vibrate( - VibrationEffect.createOneShot( - millis, - VibrationEffect.DEFAULT_AMPLITUDE - ) - ) - } else { - @Suppress("deprecation") - this.vibrate(millis) // Deprecated in API 26 but still works for lower versions - } - } - -} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 07d5da9..5cc10bf 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,170 +1,170 @@ - + xmlns:android="http://schemas.android.com/apk/res/android"> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> + android:strokeColor="#33FFFFFF" + android:strokeWidth="0.8" /> diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index eca70cf..00f9eaa 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index eca70cf..00f9eaa 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78..faf880a 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..15e0108 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d..22db1b2 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d6..6bb4df4 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..d49bd19 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 62b611d..2c58a8c 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a307..3228997 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..87e923c Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a695..f342892 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77..364721f 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..8a9af4c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f50..92f9f07 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d642..043d2fc 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..edf2d9f Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 9126ae3..2914d00 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6b0e81b..7003d0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,25 @@ Volume Key Track Control + + Vibration Settings + Long Press Settings + + Predefined Vibration + Manual Vibration + + Click + Double Click + Heavy Click + Tick + + Vibration Length: %dms + Test Vibration + + Long Press Duration: %dms + + Restore Default + Settings are restored to default + About + + Module is not enabled in LSPosed diff --git a/build.gradle b/build.gradle deleted file mode 100644 index a15e35e..0000000 --- a/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -plugins { - id 'com.android.application' version '8.7.3' apply false - id 'com.android.library' version '8.7.3' apply false - id 'org.jetbrains.kotlin.android' version '2.1.0' apply false -} - -String versionName = "1.15.2" -Integer versionCode = 9 - -rootProject.ext.set("appVersionName", versionName) -rootProject.ext.set("appVersionCode", versionCode) - -tasks.register("getVersion") { - doLast { - File versionFile = new File("app/build/version.txt") - versionFile.parentFile.mkdirs() - if (!versionFile.exists()) { - versionFile.createNewFile() - } - versionFile.setText(versionName) - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..51d1dfc --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("com.android.application") version "8.7.3" apply false + id("com.android.library") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false +} + +val versionName = "1.15.2" +val versionCode = 9 + +rootProject.ext.set("appVersionName", versionName) +rootProject.ext.set("appVersionCode", versionCode) + +tasks.register("getVersion") { + doLast { + val versionFile = file("app/build/version.txt") + versionFile.parentFile.mkdirs() + if (!versionFile.exists()) { + versionFile.createNewFile() + } + versionFile.writeText(versionName) + } +} \ No newline at end of file