diff --git a/DisableSounds/Readme.md b/DisableSounds/Readme.md
new file mode 100644
index 0000000..27ad570
--- /dev/null
+++ b/DisableSounds/Readme.md
@@ -0,0 +1,21 @@
+# DisableSounds
+
+Disable various system sounds.
+
+- Disable (regionally) forced camera sound
+- Disable shutter sound
+- Disable screenshot sound
+- Disable charging sound and vibration
+
+## Forced camera sound
+
+In some regions the camera sounds
+for shutter, start/stop recording and focus
+are enforced by law.
+This module tells the system that disabling the sounds
+is allowed.
+The system and apps will then show/enable their options
+for disabling camera/shutter sounds.
+This hook is always enabled, since allowing the option to appear,
+does not change the option by default.
+Be aware that it might be illegal to disable camera sounds in your country.
diff --git a/DisableSounds/build.gradle.kts b/DisableSounds/build.gradle.kts
new file mode 100644
index 0000000..7541d88
--- /dev/null
+++ b/DisableSounds/build.gradle.kts
@@ -0,0 +1,23 @@
+plugins {
+ alias(libs.plugins.buildlogic.android.application)
+ alias(libs.plugins.buildlogic.kotlin.android)
+}
+
+android {
+ namespace = "com.programminghoch10.DisableSounds"
+
+ defaultConfig {
+ minSdk = 17
+ targetSdk = 36
+ buildConfigField("String", "SHARED_PREFERENCES_NAME", "\"disable_sounds\"")
+ }
+}
+
+dependencies {
+ // fragment-ktx is included as transitive dependency through preference-ktx
+ // the transitive dependency is a lower version though, which allows minSdk 17,
+ // while explicit mention with the latest version forced minSdk 21
+ //implementation(libs.androidx.fragment.ktx)
+ implementation(libs.androidx.preference.ktx)
+ implementation(libs.kotlinx.coroutines.guava)
+}
diff --git a/DisableSounds/src/main/AndroidManifest.xml b/DisableSounds/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b3547a0
--- /dev/null
+++ b/DisableSounds/src/main/AndroidManifest.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DisableSounds/src/main/assets/xposed_init b/DisableSounds/src/main/assets/xposed_init
new file mode 100644
index 0000000..0f991eb
--- /dev/null
+++ b/DisableSounds/src/main/assets/xposed_init
@@ -0,0 +1,4 @@
+com.programminghoch10.DisableSounds.DisableChargingSoundsHook
+com.programminghoch10.DisableSounds.DisableForcedCameraSoundsHook
+com.programminghoch10.DisableSounds.DisableScreenshotSoundsHook
+com.programminghoch10.DisableSounds.DisableShutterSoundsHook
diff --git a/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/DisableChargingSoundsHook.kt b/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/DisableChargingSoundsHook.kt
new file mode 100644
index 0000000..ad89228
--- /dev/null
+++ b/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/DisableChargingSoundsHook.kt
@@ -0,0 +1,61 @@
+package com.programminghoch10.DisableSounds
+
+import android.content.ContentResolver
+import android.provider.Settings
+import com.programminghoch10.DisableSounds.BuildConfig.SHARED_PREFERENCES_NAME
+import de.robv.android.xposed.IXposedHookLoadPackage
+import de.robv.android.xposed.XC_MethodHook
+import de.robv.android.xposed.XC_MethodReplacement
+import de.robv.android.xposed.XSharedPreferences
+import de.robv.android.xposed.XposedHelpers
+import de.robv.android.xposed.callbacks.XC_LoadPackage
+
+class DisableChargingSoundsHook : IXposedHookLoadPackage {
+ override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
+ if (lpparam.packageName != "android") return
+ val sharedPreferences = XSharedPreferences(BuildConfig.APPLICATION_ID, SHARED_PREFERENCES_NAME)
+ if (!sharedPreferences.getBoolean("charging", false)) return
+ val disableChargingFeedback = sharedPreferences.getBoolean("chargingFeedback", false)
+
+ val CHARGING_STARTED_SOUND = XposedHelpers.getStaticObjectField(Settings::class.java, "CHARGING_STARTED_SOUND") as String
+ val WIRELESS_CHARGING_STARTED_SOUND = XposedHelpers.getStaticObjectField(Settings::class.java, "WIRELESS_CHARGING_STARTED_SOUND") as String
+ val CHARGING_VIBRATION_ENABLED = XposedHelpers.getStaticObjectField(Settings.Secure::class.java, "CHARGING_VIBRATION_ENABLED") as String
+
+ XposedHelpers.findAndHookMethod(
+ Settings.Global::class.java,
+ "getString",
+ ContentResolver::class.java,
+ String::class.java,
+ object : XC_MethodHook() {
+ override fun beforeHookedMethod(param: MethodHookParam) {
+ val string = param.args[1] as String
+ if (string == CHARGING_STARTED_SOUND || string == WIRELESS_CHARGING_STARTED_SOUND) param.result = null
+ }
+ },
+ )
+
+ if (disableChargingFeedback) {
+ XposedHelpers.findAndHookMethod(
+ Settings.Secure::class.java,
+ "getIntForUser",
+ ContentResolver::class.java,
+ String::class.java,
+ Int::class.java,
+ object : XC_MethodHook() {
+ override fun beforeHookedMethod(param: MethodHookParam) {
+ val string = param.args[1] as String
+ if (string == CHARGING_VIBRATION_ENABLED) param.result = 0
+ }
+ },
+ )
+
+ val NotifierClass = XposedHelpers.findClass("com.android.server.power.Notifier", lpparam.classLoader)
+ XposedHelpers.findAndHookMethod(
+ NotifierClass,
+ "isChargingFeedbackEnabled",
+ Int::class.java,
+ XC_MethodReplacement.returnConstant(false),
+ )
+ }
+ }
+}
diff --git a/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/DisableForcedCameraSoundsHook.kt b/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/DisableForcedCameraSoundsHook.kt
new file mode 100644
index 0000000..e55c27d
--- /dev/null
+++ b/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/DisableForcedCameraSoundsHook.kt
@@ -0,0 +1,31 @@
+package com.programminghoch10.DisableSounds
+
+import android.content.res.XResources
+import android.media.MediaActionSound
+import android.os.Build
+import android.util.Log
+import de.robv.android.xposed.IXposedHookLoadPackage
+import de.robv.android.xposed.IXposedHookZygoteInit
+import de.robv.android.xposed.XC_MethodReplacement
+import de.robv.android.xposed.XposedHelpers
+import de.robv.android.xposed.callbacks.XC_LoadPackage
+
+class DisableForcedCameraSoundsHook : IXposedHookLoadPackage, IXposedHookZygoteInit {
+ override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
+ Log.d(this::class.java.simpleName, "handleLoadPackage: loaded ${this::class.java.simpleName} with package ${lpparam.packageName}")
+ if (lpparam.packageName == "android") {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ val AudioServiceClass = XposedHelpers.findClass("com.android.server.audio.AudioService", lpparam.classLoader)
+ XposedHelpers.findAndHookMethod(AudioServiceClass, "isCameraSoundForced", XC_MethodReplacement.returnConstant(false))
+ XposedHelpers.findAndHookMethod(AudioServiceClass, "readCameraSoundForced", XC_MethodReplacement.returnConstant(false))
+ }
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ XposedHelpers.findAndHookMethod(MediaActionSound::class.java, "mustPlayShutterSound", XC_MethodReplacement.returnConstant(false))
+ }
+ }
+
+ override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
+ XResources.setSystemWideReplacement("android", "bool", "config_camera_sound_forced", false)
+ }
+}
diff --git a/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/DisableScreenshotSoundsHook.kt b/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/DisableScreenshotSoundsHook.kt
new file mode 100644
index 0000000..860cbbd
--- /dev/null
+++ b/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/DisableScreenshotSoundsHook.kt
@@ -0,0 +1,64 @@
+package com.programminghoch10.DisableSounds
+
+import android.content.Context
+import android.media.MediaActionSound
+import android.os.Build
+import com.google.common.util.concurrent.Futures
+import com.programminghoch10.DisableSounds.BuildConfig.SHARED_PREFERENCES_NAME
+import de.robv.android.xposed.IXposedHookLoadPackage
+import de.robv.android.xposed.XC_MethodHook
+import de.robv.android.xposed.XC_MethodReplacement
+import de.robv.android.xposed.XSharedPreferences
+import de.robv.android.xposed.XposedHelpers
+import de.robv.android.xposed.callbacks.XC_LoadPackage
+
+class DisableScreenshotSoundsHook : IXposedHookLoadPackage {
+ override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
+ if (lpparam.packageName != "com.android.systemui") return
+ val sharedPreferences = XSharedPreferences(BuildConfig.APPLICATION_ID, SHARED_PREFERENCES_NAME)
+ if (!sharedPreferences.getBoolean("screenshot", false)) return
+
+ when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> XposedHelpers.findAndHookMethod(
+ "com.android.systemui.screenshot.ScreenshotSoundControllerImpl",
+ lpparam.classLoader,
+ "playScreenshotSoundAsync",
+ XC_MethodReplacement.DO_NOTHING,
+ )
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
+ // TODO: check if inlined by r8 on 33
+ XposedHelpers.findAndHookMethod(
+ "com.android.systemui.screenshot.ScreenshotController",
+ lpparam.classLoader,
+ "playCameraSound",
+ XC_MethodReplacement.DO_NOTHING,
+ )
+
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
+ val ScreenshotControllerClass = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ XposedHelpers.findClass("com.android.systemui.screenshot.ScreenshotController", lpparam.classLoader)
+ } else {
+ XposedHelpers.findClass("com.android.systemui.screenshot.GlobalScreenshot", lpparam.classLoader)
+ }
+
+ var replacementDummy: Any = MediaActionSoundDummy()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) replacementDummy = Futures.immediateFuture(replacementDummy)
+
+ XposedHelpers.findAndHookConstructor(
+ ScreenshotControllerClass, Context::class.java, object : XC_MethodHook() {
+ override fun afterHookedMethod(param: MethodHookParam) {
+ XposedHelpers.setObjectField(
+ param.thisObject, "mCameraSound", replacementDummy
+ )
+ }
+ })
+ }
+ }
+
+ class MediaActionSoundDummy : MediaActionSound() {
+ override fun load(soundName: Int) {}
+ override fun play(soundName: Int) {}
+ override fun release() {}
+ }
+}
diff --git a/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/DisableShutterSoundsHook.kt b/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/DisableShutterSoundsHook.kt
new file mode 100644
index 0000000..3348e8a
--- /dev/null
+++ b/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/DisableShutterSoundsHook.kt
@@ -0,0 +1,32 @@
+package com.programminghoch10.DisableSounds
+
+import android.media.MediaActionSound
+import android.os.Build
+import com.programminghoch10.DisableSounds.BuildConfig.SHARED_PREFERENCES_NAME
+import de.robv.android.xposed.IXposedHookLoadPackage
+import de.robv.android.xposed.XC_MethodReplacement
+import de.robv.android.xposed.XSharedPreferences
+import de.robv.android.xposed.XposedHelpers
+import de.robv.android.xposed.callbacks.XC_LoadPackage
+
+class DisableShutterSoundsHook : IXposedHookLoadPackage {
+ override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam?) {
+ val sharedPreferences = XSharedPreferences(BuildConfig.APPLICATION_ID, SHARED_PREFERENCES_NAME)
+ if (!sharedPreferences.getBoolean("shutter", false)) return
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ for (methodName in listOf("load", "play")) {
+ XposedHelpers.findAndHookMethod(
+ MediaActionSound::class.java,
+ methodName,
+ Int::class.java,
+ XC_MethodReplacement.DO_NOTHING,
+ )
+ }
+ }
+
+ // TODO: need a native hook for old Camera API methods
+ // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/av/services/camera/libcameraservice/CameraService.cpp;l=4097?q=camera_click.ogg
+ // then the "might not work" warning can be removed from @string/disable_shutter_sounds_description_on
+ }
+}
diff --git a/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/SettingsActivity.kt b/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/SettingsActivity.kt
new file mode 100644
index 0000000..e77dd84
--- /dev/null
+++ b/DisableSounds/src/main/java/com/programminghoch10/DisableSounds/SettingsActivity.kt
@@ -0,0 +1,30 @@
+package com.programminghoch10.DisableSounds
+
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+import androidx.preference.PreferenceFragmentCompat
+import com.programminghoch10.DisableSounds.BuildConfig.SHARED_PREFERENCES_NAME
+
+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 true
+ }
+
+ class SettingsFragment : PreferenceFragmentCompat() {
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ preferenceManager.sharedPreferencesName = SHARED_PREFERENCES_NAME
+ preferenceManager.sharedPreferencesMode = MODE_WORLD_READABLE
+ setPreferencesFromResource(R.xml.root_preferences, rootKey)
+ }
+ }
+}
diff --git a/DisableSounds/src/main/res/layout/settings_activity.xml b/DisableSounds/src/main/res/layout/settings_activity.xml
new file mode 100644
index 0000000..d4662df
--- /dev/null
+++ b/DisableSounds/src/main/res/layout/settings_activity.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/DisableSounds/src/main/res/values-v21/themes.xml b/DisableSounds/src/main/res/values-v21/themes.xml
new file mode 100644
index 0000000..ee02adb
--- /dev/null
+++ b/DisableSounds/src/main/res/values-v21/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/DisableSounds/src/main/res/values-v34/strings.xml b/DisableSounds/src/main/res/values-v34/strings.xml
new file mode 100644
index 0000000..6f6cd64
--- /dev/null
+++ b/DisableSounds/src/main/res/values-v34/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Some apps using the old Camera API might not be muted.
+
diff --git a/DisableSounds/src/main/res/values/arrays.xml b/DisableSounds/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..38ca316
--- /dev/null
+++ b/DisableSounds/src/main/res/values/arrays.xml
@@ -0,0 +1,8 @@
+
+
+
+ - android
+ - com.android.systemui
+ - org.lineageos.aperture
+
+
diff --git a/DisableSounds/src/main/res/values/strings.xml b/DisableSounds/src/main/res/values/strings.xml
new file mode 100644
index 0000000..0c8c82d
--- /dev/null
+++ b/DisableSounds/src/main/res/values/strings.xml
@@ -0,0 +1,14 @@
+
+
+ DisableSounds Configuration
+ DisableSounds
+ Disable various system sounds
+ Disable screenshot sounds
+ Disable screenshot and screen recording sounds.
+ Disable shutter sounds
+ Disable shutter, focus and video recording sounds.
+ Some apps using the old Camera API might not be muted. The screenshot sound might be muted too.
+ Disable charging sounds
+ Disable charging feedback
+ Also disable charging vibration.
+
diff --git a/DisableSounds/src/main/res/values/themes.xml b/DisableSounds/src/main/res/values/themes.xml
new file mode 100644
index 0000000..7e8afbb
--- /dev/null
+++ b/DisableSounds/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/DisableSounds/src/main/res/xml/root_preferences.xml b/DisableSounds/src/main/res/xml/root_preferences.xml
new file mode 100644
index 0000000..8c43548
--- /dev/null
+++ b/DisableSounds/src/main/res/xml/root_preferences.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 14890dc..39b3988 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -10,6 +10,7 @@ githubApi = "1.330"
hiddenapibypass = "6.1"
jebrainsAnnotations = "26.0.2-1"
kotlin = "2.2.20"
+kotlinxCoroutinesGuava = "1.10.2"
libsu = "6.0.0"
preference = "1.2.1"
xposed = "82"
@@ -30,6 +31,7 @@ jebtrains-annotations = { module = "org.jetbrains:annotations", version.ref = "j
kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin" }
kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" }
+kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" }
libsu-core = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" }
xposed-api = { module = "de.robv.android.xposed:api", version.ref = "xposed" }
diff --git a/metadata/com.programminghoch10.DisableSounds/en-US/full_description.txt b/metadata/com.programminghoch10.DisableSounds/en-US/full_description.txt
new file mode 100644
index 0000000..0fbf9e2
--- /dev/null
+++ b/metadata/com.programminghoch10.DisableSounds/en-US/full_description.txt
@@ -0,0 +1,6 @@
+Disable various system sounds.
+
+- Disable (regionally) forced camera sound
+- Disable shutter sound
+- Disable screenshot sound
+- Disable charging sound and vibration
diff --git a/metadata/com.programminghoch10.DisableSounds/en-US/short_description.txt b/metadata/com.programminghoch10.DisableSounds/en-US/short_description.txt
new file mode 100644
index 0000000..e65c164
--- /dev/null
+++ b/metadata/com.programminghoch10.DisableSounds/en-US/short_description.txt
@@ -0,0 +1 @@
+Disable various system sounds.
\ No newline at end of file
diff --git a/metadata/com.programminghoch10.DisableSounds/en-US/title.txt b/metadata/com.programminghoch10.DisableSounds/en-US/title.txt
new file mode 100644
index 0000000..eb487d0
--- /dev/null
+++ b/metadata/com.programminghoch10.DisableSounds/en-US/title.txt
@@ -0,0 +1 @@
+DisableSounds
\ No newline at end of file
diff --git a/modules.gradle.kts b/modules.gradle.kts
index a3d053f..c0f785e 100644
--- a/modules.gradle.kts
+++ b/modules.gradle.kts
@@ -7,6 +7,7 @@ include(":BetterBluetoothDeviceSort")
include(":BetterVerboseWiFiLogging")
include(":ClassHunter")
include(":CodecMod")
+include(":DisableSounds")
include(":DontResetIfBootedAndConnected")
include(":EnableCallRecording")
include(":FreeNotifications")