diff --git a/README.md b/README.md
index 9bc9877..8867bb8 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@ A collection of small Xposed Modules.
| [ResetAllNotificationChannels](ResetAllNotificationChannels) | [@binarynoise](https://github.com/binarynoise) | Reset all Notification Channels: vibrations, ringtones, importance etc. | [GitHub](https://github.com/binarynoise/XposedModulets/releases?q=resetAllNotificationChannels) [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/de.binarynoise.resetAllNotificationChannels) |
| [RotationControl](RotationControl) | [@programminghoch10](https://github.com/programminghoch10) | Force rotation for selected packages | [GitHub](https://github.com/binarynoise/XposedModulets/releases?q=RotationControl) [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.programminghoch10.RotationControl) |
| [UpsideWifi](UpsideWifi) | [@programminghoch10](https://github.com/programminghoch10) | Turn the WiFi icon upside down | [GitHub](https://github.com/binarynoise/XposedModulets/releases?q=UpsideWifi) |
+| [VolumeStepsIncrease](VolumeStepsIncrease) | [@programminghoch10](https://github.com/programminghoch10) | Increase the amount of volume steps | [GitHub](https://github.com/binarynoise/XposedModulets/releases?q=VolumeStepsIncrease) |
## License
diff --git a/VolumeStepsIncrease/Readme.md b/VolumeStepsIncrease/Readme.md
new file mode 100644
index 0000000..cfe8bb2
--- /dev/null
+++ b/VolumeStepsIncrease/Readme.md
@@ -0,0 +1,3 @@
+# VolumeStepsIncrease
+
+Increase the amount of volume steps.
diff --git a/VolumeStepsIncrease/build.gradle.kts b/VolumeStepsIncrease/build.gradle.kts
new file mode 100644
index 0000000..48b1ab2
--- /dev/null
+++ b/VolumeStepsIncrease/build.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+ alias(libs.plugins.buildlogic.android.application)
+ alias(libs.plugins.buildlogic.kotlin.android)
+}
+
+android {
+ namespace = "com.programminghoch10.VolumeStepsIncrease"
+
+ defaultConfig {
+ minSdk = 23
+ targetSdk = 35
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.preference.ktx)
+}
diff --git a/VolumeStepsIncrease/src/main/AndroidManifest.xml b/VolumeStepsIncrease/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b8980a2
--- /dev/null
+++ b/VolumeStepsIncrease/src/main/AndroidManifest.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/VolumeStepsIncrease/src/main/assets/xposed_init b/VolumeStepsIncrease/src/main/assets/xposed_init
new file mode 100644
index 0000000..2d70ba7
--- /dev/null
+++ b/VolumeStepsIncrease/src/main/assets/xposed_init
@@ -0,0 +1 @@
+com.programminghoch10.VolumeStepsIncrease.Hook
diff --git a/VolumeStepsIncrease/src/main/java/com/programminghoch10/VolumeStepsIncrease/Common.kt b/VolumeStepsIncrease/src/main/java/com/programminghoch10/VolumeStepsIncrease/Common.kt
new file mode 100644
index 0000000..93b579c
--- /dev/null
+++ b/VolumeStepsIncrease/src/main/java/com/programminghoch10/VolumeStepsIncrease/Common.kt
@@ -0,0 +1,61 @@
+package com.programminghoch10.VolumeStepsIncrease
+
+import com.programminghoch10.VolumeStepsIncrease.StockValues.MAX_STREAM_VOLUME
+import com.programminghoch10.VolumeStepsIncrease.StockValues.MIN_STREAM_VOLUME
+import com.programminghoch10.VolumeStepsIncrease.StockValues.STREAM_ALARM
+import com.programminghoch10.VolumeStepsIncrease.StockValues.STREAM_MUSIC
+import com.programminghoch10.VolumeStepsIncrease.StockValues.STREAM_SYSTEM
+import com.programminghoch10.VolumeStepsIncrease.StockValues.STREAM_VOICE_CALL
+
+object Common {
+ const val SHARED_PREFERENCES_NAME = "streams"
+
+ val STREAMS = StockValues::class.java.declaredFields.filter { it.name.startsWith("STREAM_") }.associate { it.name to it.getInt(null) }
+
+ val systemPropertyToStream = mapOf(
+ "ro.config.vc_call_vol_steps" to STREAM_VOICE_CALL,
+ "ro.config.media_vol_steps" to STREAM_MUSIC,
+ "ro.config.alarm_vol_steps" to STREAM_ALARM,
+ "ro.config.system_vol_steps" to STREAM_SYSTEM,
+ )
+ val streamsToSystemProperties = systemPropertyToStream.entries.associate { it.value to it.key }
+
+ val moduleDefaultVolumeSteps = mapOf(
+ STREAM_MUSIC to MAX_STREAM_VOLUME[STREAM_MUSIC] * 2,
+ STREAM_VOICE_CALL to MAX_STREAM_VOLUME[STREAM_VOICE_CALL] * 2,
+ )
+
+ fun getSystemMaxVolumeSteps(stream: Int): Int {
+ val default = MAX_STREAM_VOLUME[stream]
+ if (streamsToSystemProperties.contains(stream)) {
+ val systemProperty = streamsToSystemProperties[stream]
+ try {
+ val SystemPropertiesClass = Class.forName("android.os.SystemProperties")
+ val getIntMethod = SystemPropertiesClass.getMethod("getInt", String::class.java, Int::class.java)
+ return getIntMethod.invoke(null, systemProperty, default) as Int
+ } catch (_: Exception) {
+ }
+ }
+ return default
+ }
+
+ fun getModuleDefaultVolumeSteps(stream: Int): Int {
+ return moduleDefaultVolumeSteps[stream] ?: getSystemMaxVolumeSteps(stream)
+ }
+
+ fun getModuleMaxVolumeSteps(stream: Int): Int {
+ return getSystemMaxVolumeSteps(stream) * 3
+ }
+
+ fun getSystemMinVolumeSteps(stream: Int): Int {
+ return MIN_STREAM_VOLUME[stream]
+ }
+
+ fun getModuleMinVolumeSteps(stream: Int): Int {
+ return getSystemMinVolumeSteps(stream)
+ }
+
+ fun getPreferenceKey(stream: Int): String {
+ return "STREAM_${stream}"
+ }
+}
diff --git a/VolumeStepsIncrease/src/main/java/com/programminghoch10/VolumeStepsIncrease/Hook.kt b/VolumeStepsIncrease/src/main/java/com/programminghoch10/VolumeStepsIncrease/Hook.kt
new file mode 100644
index 0000000..a35707c
--- /dev/null
+++ b/VolumeStepsIncrease/src/main/java/com/programminghoch10/VolumeStepsIncrease/Hook.kt
@@ -0,0 +1,62 @@
+package com.programminghoch10.VolumeStepsIncrease
+
+import com.programminghoch10.VolumeStepsIncrease.Common.SHARED_PREFERENCES_NAME
+import com.programminghoch10.VolumeStepsIncrease.Common.STREAMS
+import com.programminghoch10.VolumeStepsIncrease.Common.getPreferenceKey
+import com.programminghoch10.VolumeStepsIncrease.Common.systemPropertyToStream
+import de.robv.android.xposed.IXposedHookLoadPackage
+import de.robv.android.xposed.XSharedPreferences
+import de.robv.android.xposed.XposedBridge
+import de.robv.android.xposed.XposedHelpers
+import de.robv.android.xposed.callbacks.XC_LoadPackage
+import de.robv.android.xposed.XC_MethodHook as MethodHook
+
+class Hook : IXposedHookLoadPackage {
+ override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
+ when (lpparam.packageName) {
+ BuildConfig.APPLICATION_ID -> return
+ "android" -> hookAndroid(lpparam)
+ else -> hookApp(lpparam)
+ }
+ }
+
+ fun hookAndroid(lpparam: XC_LoadPackage.LoadPackageParam) {
+
+ val AudioServiceClass = XposedHelpers.findClass("com.android.server.audio.AudioService", lpparam.classLoader)
+ XposedBridge.hookAllConstructors(AudioServiceClass, object : MethodHook() {
+ override fun afterHookedMethod(param: MethodHookParam) {
+ val MAX_STREAM_VOLUME = XposedHelpers.getObjectField(param.thisObject, "MAX_STREAM_VOLUME") as IntArray
+ val preferences = XSharedPreferences(BuildConfig.APPLICATION_ID, SHARED_PREFERENCES_NAME)
+ STREAMS.filter { preferences.contains(getPreferenceKey(it.value)) }
+ .map { it.value to preferences.getInt(getPreferenceKey(it.value), MAX_STREAM_VOLUME[it.value]) }
+ .forEach { MAX_STREAM_VOLUME[it.first] = it.second }
+ }
+ })
+
+ hookApp(lpparam)
+ }
+
+ fun hookApp(lpparam: XC_LoadPackage.LoadPackageParam) {
+
+ val SystemPropertiesClass = XposedHelpers.findClass("android.os.SystemProperties", lpparam.classLoader)
+
+ XposedHelpers.findAndHookMethod(SystemPropertiesClass, "getInt", String::class.java, Int::class.java, object : MethodHook() {
+ override fun afterHookedMethod(param: MethodHookParam) {
+ val key = param.args[0] as String
+ param.args[1] as Int
+ val result = param.result as Int
+ val streamInt = systemPropertyToStream[key] ?: return
+ val preferences = XSharedPreferences(BuildConfig.APPLICATION_ID, SHARED_PREFERENCES_NAME)
+ if (!preferences.contains(getPreferenceKey(streamInt))) return
+ param.result = preferences.getInt(getPreferenceKey(streamInt), result)
+ }
+ })
+
+ XposedHelpers.findAndHookMethod(SystemPropertiesClass, "getBoolean", String::class.java, Boolean::class.java, object : MethodHook() {
+ override fun beforeHookedMethod(param: MethodHookParam) {
+ val key = param.args[0] as String
+ if (key == "audio.safemedia.bypass") param.result = true
+ }
+ })
+ }
+}
diff --git a/VolumeStepsIncrease/src/main/java/com/programminghoch10/VolumeStepsIncrease/SettingsActivity.kt b/VolumeStepsIncrease/src/main/java/com/programminghoch10/VolumeStepsIncrease/SettingsActivity.kt
new file mode 100644
index 0000000..d683375
--- /dev/null
+++ b/VolumeStepsIncrease/src/main/java/com/programminghoch10/VolumeStepsIncrease/SettingsActivity.kt
@@ -0,0 +1,78 @@
+package com.programminghoch10.VolumeStepsIncrease
+
+import kotlin.math.round
+import android.annotation.SuppressLint
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceGroup
+import androidx.preference.SeekBarPreference
+import androidx.preference.children
+import com.programminghoch10.VolumeStepsIncrease.Common.SHARED_PREFERENCES_NAME
+import com.programminghoch10.VolumeStepsIncrease.Common.STREAMS
+import com.programminghoch10.VolumeStepsIncrease.Common.getModuleDefaultVolumeSteps
+import com.programminghoch10.VolumeStepsIncrease.Common.getModuleMaxVolumeSteps
+import com.programminghoch10.VolumeStepsIncrease.Common.getModuleMinVolumeSteps
+import com.programminghoch10.VolumeStepsIncrease.Common.getPreferenceKey
+import com.programminghoch10.VolumeStepsIncrease.Common.getSystemMaxVolumeSteps
+
+class SettingsActivity : FragmentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.settings_activity)
+ if (savedInstanceState == null) {
+ supportFragmentManager.beginTransaction().replace(R.id.settings, SettingsFragment()).commit()
+ }
+ actionBar?.setDisplayHomeAsUpEnabled(true)
+ }
+
+ override fun onNavigateUp(): Boolean {
+ finish()
+ return super.onNavigateUp()
+ }
+
+ class SettingsFragment : PreferenceFragmentCompat() {
+ @SuppressLint("WorldReadableFiles")
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.root_preferences, rootKey)
+ preferenceManager.sharedPreferencesName = SHARED_PREFERENCES_NAME
+ preferenceManager.sharedPreferencesMode = MODE_WORLD_READABLE
+
+ for (stream in STREAMS) {
+ val preference = SeekBarPreference(requireContext())
+ preference.key = getPreferenceKey(stream.value)
+ preference.title = stream.key.replace("STREAM_", "")
+ preference.min = getModuleMinVolumeSteps(stream.value)
+ preference.max = getModuleMaxVolumeSteps(stream.value)
+ preference.setDefaultValue(getModuleDefaultVolumeSteps(stream.value))
+ preference.showSeekBarValue = true
+ preference.updatesContinuously = true
+ preference.setOnPreferenceChangeListener { preference, newValue ->
+ val factor = (newValue as Int).toDouble() / getSystemMaxVolumeSteps(stream.value)
+ preference.summary = arrayOf(
+ "factor=${factor.round(1)}x ",
+ //"systemMin=${getSystemMinVolumeSteps(stream.value)} ",
+ "systemMax=${getSystemMaxVolumeSteps(stream.value)} ",
+ ).joinToString(" ")
+ true
+ }
+ preferenceScreen.addPreference(preference)
+ preference.onPreferenceChangeListener?.onPreferenceChange(preference, preference.value)
+ }
+ preferenceScreen.setIconSpaceReservedRecursive(false)
+ }
+
+ fun Preference.setIconSpaceReservedRecursive(iconSpaceReserved: Boolean) {
+ this.isIconSpaceReserved = iconSpaceReserved
+ if (this is PreferenceGroup) children.forEach { it.setIconSpaceReservedRecursive(iconSpaceReserved) }
+ }
+
+ fun Double.round(decimals: Int = 0): Double {
+ var multiplier = 1.0
+ repeat(decimals) { multiplier *= 10 }
+ return round(this * multiplier) / multiplier
+ }
+ }
+}
diff --git a/VolumeStepsIncrease/src/main/java/com/programminghoch10/VolumeStepsIncrease/StockValues.kt b/VolumeStepsIncrease/src/main/java/com/programminghoch10/VolumeStepsIncrease/StockValues.kt
new file mode 100644
index 0000000..6bbfce1
--- /dev/null
+++ b/VolumeStepsIncrease/src/main/java/com/programminghoch10/VolumeStepsIncrease/StockValues.kt
@@ -0,0 +1,94 @@
+package com.programminghoch10.VolumeStepsIncrease
+
+import android.media.AudioManager
+import androidx.annotation.Keep
+
+@Keep
+object StockValues {
+
+ // from com.android.server.media.AudioService
+
+ /** Maximum volume index values for audio streams */
+ var MAX_STREAM_VOLUME: IntArray = intArrayOf(
+ 5, // STREAM_VOICE_CALL
+ 7, // STREAM_SYSTEM
+ 7, // STREAM_RING
+ 15, // STREAM_MUSIC
+ 7, // STREAM_ALARM
+ 7, // STREAM_NOTIFICATION
+ 15, // STREAM_BLUETOOTH_SCO
+ 7, // STREAM_SYSTEM_ENFORCED
+ 15, // STREAM_DTMF
+ 15, // STREAM_TTS
+ 15, // STREAM_ACCESSIBILITY
+ 15, // STREAM_ASSISTANT
+ )
+
+ /** Minimum volume index values for audio streams */
+ var MIN_STREAM_VOLUME: IntArray = intArrayOf(
+ 1, // STREAM_VOICE_CALL
+ 0, // STREAM_SYSTEM
+ 0, // STREAM_RING
+ 0, // STREAM_MUSIC
+ 1, // STREAM_ALARM
+ 0, // STREAM_NOTIFICATION
+ 0, // STREAM_BLUETOOTH_SCO
+ 0, // STREAM_SYSTEM_ENFORCED
+ 0, // STREAM_DTMF
+ 0, // STREAM_TTS
+ 1, // STREAM_ACCESSIBILITY
+ 0, // STREAM_ASSISTANT
+ )
+
+ // from android.media.AudioSystem
+
+ const val STREAM_VOICE_CALL: Int = AudioManager.STREAM_VOICE_CALL
+
+ /** @hide Used to identify the volume of audio streams for system sounds
+ */
+ const val STREAM_SYSTEM: Int = AudioManager.STREAM_SYSTEM
+
+ /** @hide Used to identify the volume of audio streams for the phone ring and message alerts
+ */
+ const val STREAM_RING: Int = AudioManager.STREAM_RING
+
+ /** @hide Used to identify the volume of audio streams for music playback
+ */
+ const val STREAM_MUSIC: Int = AudioManager.STREAM_MUSIC
+
+ /** @hide Used to identify the volume of audio streams for alarms
+ */
+ const val STREAM_ALARM: Int = AudioManager.STREAM_ALARM
+
+ /** @hide Used to identify the volume of audio streams for notifications
+ */
+ const val STREAM_NOTIFICATION: Int = AudioManager.STREAM_NOTIFICATION
+
+ /** @hide
+ * Used to identify the volume of audio streams for phone calls when connected on bluetooth
+ */
+ @Deprecated("use {@link #STREAM_VOICE_CALL} instead ")
+ const val STREAM_BLUETOOTH_SCO: Int = 6
+
+ /** @hide Used to identify the volume of audio streams for enforced system sounds in certain
+ * countries (e.g camera in Japan)
+ */
+ const val STREAM_SYSTEM_ENFORCED: Int = 7
+
+ /** @hide Used to identify the volume of audio streams for DTMF tones
+ */
+ const val STREAM_DTMF: Int = AudioManager.STREAM_DTMF
+
+ /** @hide Used to identify the volume of audio streams exclusively transmitted through the
+ * speaker (TTS) of the device
+ */
+ const val STREAM_TTS: Int = 9
+
+ /** @hide Used to identify the volume of audio streams for accessibility prompts
+ */
+ const val STREAM_ACCESSIBILITY: Int = AudioManager.STREAM_ACCESSIBILITY
+
+ /** @hide Used to identify the volume of audio streams for virtual assistant
+ */
+ const val STREAM_ASSISTANT: Int = 11
+}
diff --git a/VolumeStepsIncrease/src/main/res/layout/settings_activity.xml b/VolumeStepsIncrease/src/main/res/layout/settings_activity.xml
new file mode 100644
index 0000000..620409c
--- /dev/null
+++ b/VolumeStepsIncrease/src/main/res/layout/settings_activity.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/VolumeStepsIncrease/src/main/res/values-v21/themes.xml b/VolumeStepsIncrease/src/main/res/values-v21/themes.xml
new file mode 100644
index 0000000..6ee0359
--- /dev/null
+++ b/VolumeStepsIncrease/src/main/res/values-v21/themes.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/VolumeStepsIncrease/src/main/res/values/arrays.xml b/VolumeStepsIncrease/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..db6e46a
--- /dev/null
+++ b/VolumeStepsIncrease/src/main/res/values/arrays.xml
@@ -0,0 +1,6 @@
+
+
+
+ - android
+
+
diff --git a/VolumeStepsIncrease/src/main/res/values/strings.xml b/VolumeStepsIncrease/src/main/res/values/strings.xml
new file mode 100644
index 0000000..d239e46
--- /dev/null
+++ b/VolumeStepsIncrease/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+
+ VolumeStepsIncrease
+ Volume Steps Configuration
+ Increase the amount of volume steps.
+ A restart is required to apply the new values!
+
diff --git a/VolumeStepsIncrease/src/main/res/values/themes.xml b/VolumeStepsIncrease/src/main/res/values/themes.xml
new file mode 100644
index 0000000..1b28f45
--- /dev/null
+++ b/VolumeStepsIncrease/src/main/res/values/themes.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/VolumeStepsIncrease/src/main/res/xml/root_preferences.xml b/VolumeStepsIncrease/src/main/res/xml/root_preferences.xml
new file mode 100644
index 0000000..bdbca14
--- /dev/null
+++ b/VolumeStepsIncrease/src/main/res/xml/root_preferences.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/metadata/com.programminghoch10.VolumeStepsIncrease/en-US/full_description.txt b/metadata/com.programminghoch10.VolumeStepsIncrease/en-US/full_description.txt
new file mode 100644
index 0000000..4977cfb
--- /dev/null
+++ b/metadata/com.programminghoch10.VolumeStepsIncrease/en-US/full_description.txt
@@ -0,0 +1 @@
+Increase the amount of volume steps.
diff --git a/metadata/com.programminghoch10.VolumeStepsIncrease/en-US/short_description.txt b/metadata/com.programminghoch10.VolumeStepsIncrease/en-US/short_description.txt
new file mode 100644
index 0000000..4977cfb
--- /dev/null
+++ b/metadata/com.programminghoch10.VolumeStepsIncrease/en-US/short_description.txt
@@ -0,0 +1 @@
+Increase the amount of volume steps.
diff --git a/metadata/com.programminghoch10.VolumeStepsIncrease/en-US/title.txt b/metadata/com.programminghoch10.VolumeStepsIncrease/en-US/title.txt
new file mode 100644
index 0000000..ac5f80f
--- /dev/null
+++ b/metadata/com.programminghoch10.VolumeStepsIncrease/en-US/title.txt
@@ -0,0 +1 @@
+VolumeStepsIncrease
\ No newline at end of file
diff --git a/modules.gradle.kts b/modules.gradle.kts
index 352f7fa..b1f4cf1 100644
--- a/modules.gradle.kts
+++ b/modules.gradle.kts
@@ -19,3 +19,4 @@ include(":ResetAllNotificationChannels")
include(":RotationControl")
include(":SensorMod")
include(":UpsideWifi")
+include(":VolumeStepsIncrease")