diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 15f5e7bd0c..9a295cf22b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -16,6 +16,10 @@
tools:ignore="ProtectedPermissions" />
+
+
diff --git a/app/src/main/java/com/sevtinge/hyperceiler/module/app/MiSound.java b/app/src/main/java/com/sevtinge/hyperceiler/module/app/MiSound.java
index bfd816d4ec..2b4f5ed027 100644
--- a/app/src/main/java/com/sevtinge/hyperceiler/module/app/MiSound.java
+++ b/app/src/main/java/com/sevtinge/hyperceiler/module/app/MiSound.java
@@ -1,25 +1,26 @@
/*
- * This file is part of HyperCeiler.
+ * This file is part of HyperCeiler.
- * HyperCeiler is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License.
+ * HyperCeiler is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License.
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
- * Copyright (C) 2023-2024 HyperCeiler Contributions
-*/
+ * Copyright (C) 2023-2024 HyperCeiler Contributions
+ */
package com.sevtinge.hyperceiler.module.app;
import com.sevtinge.hyperceiler.module.base.BaseModule;
import com.sevtinge.hyperceiler.module.base.HookExpand;
+import com.sevtinge.hyperceiler.module.hook.misound.BluetoothListener;
import com.sevtinge.hyperceiler.module.hook.misound.IncreaseSamplingRate;
@HookExpand(pkg = "com.miui.misound", isPad = false, tarAndroid = 33)
@@ -27,6 +28,7 @@ public class MiSound extends BaseModule {
@Override
public void handleLoadPackage() {
+ initHook(new BluetoothListener(), mPrefsMap.getBoolean("misound_bluetooth"));
initHook(IncreaseSamplingRate.INSTANCE, mPrefsMap.getBoolean("misound_increase_sampling_rate"));
}
}
diff --git a/app/src/main/java/com/sevtinge/hyperceiler/module/hook/misound/BluetoothListener.java b/app/src/main/java/com/sevtinge/hyperceiler/module/hook/misound/BluetoothListener.java
new file mode 100644
index 0000000000..7e08018519
--- /dev/null
+++ b/app/src/main/java/com/sevtinge/hyperceiler/module/hook/misound/BluetoothListener.java
@@ -0,0 +1,226 @@
+package com.sevtinge.hyperceiler.module.hook.misound;
+
+import android.app.Application;
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+
+import com.sevtinge.hyperceiler.module.base.BaseHook;
+import com.sevtinge.hyperceiler.module.base.dexkit.DexKit;
+
+import org.luckypray.dexkit.query.FindClass;
+import org.luckypray.dexkit.query.FindField;
+import org.luckypray.dexkit.query.matchers.ClassMatcher;
+import org.luckypray.dexkit.query.matchers.FieldMatcher;
+import org.luckypray.dexkit.result.ClassData;
+import org.luckypray.dexkit.result.FieldData;
+
+import java.util.UUID;
+
+import de.robv.android.xposed.XposedHelpers;
+
+public class BluetoothListener extends BaseHook {
+ private static final String TAG = "BluetoothListener";
+ private static Object miDolby = null;
+ private static Object miAudio = null;
+ private static String uuid = "";
+
+ @Override
+ public void init() throws NoSuchMethodException {
+ uuid = mPrefsMap.getString("misound_bluetooth_uuid", "");
+ ClassData classData = DexKit.getDexKitBridge().findClass(FindClass.create().matcher(ClassMatcher.create()
+ .usingStrings("Creating a DolbyAudioEffect to global output mix!"))).singleOrNull();
+ try {
+ if (classData == null) {
+ logE(TAG, "AudioEffect not found");
+ } else {
+ findAndHookConstructor(classData.getInstance(lpparam.classLoader),
+ int.class, int.class,
+ new MethodHook() {
+ @Override
+ protected void after(MethodHookParam param) {
+ miDolby = param.thisObject;
+ // logE(TAG, "miDolby: " + miDolby);
+ }
+ }
+ );
+ }
+ } catch (ClassNotFoundException e) {
+ logE(TAG, e);
+ }
+ ClassData classData1 = DexKit.getDexKitBridge().findClass(FindClass.create().matcher(ClassMatcher.create()
+ .usingStrings("android.media.audiofx.MiSound"))).singleOrNull();
+ try {
+ if (classData1 == null) {
+ logE(TAG, "MiSound not found");
+ } else {
+ FieldData fieldData = DexKit.getDexKitBridge().findField(FindField.create().matcher(FieldMatcher.create()
+ .declaredClass(classData1.getInstance(lpparam.classLoader)).type(Object.class)
+ )).singleOrNull();
+ if (fieldData == null) {
+ logE(TAG, "field not found");
+ } else {
+ String name = fieldData.getFieldName();
+ findAndHookConstructor(classData1.getInstance(lpparam.classLoader),
+ int.class, int.class,
+ new MethodHook() {
+ @Override
+ protected void after(MethodHookParam param) {
+ miAudio = XposedHelpers.getObjectField(param.thisObject, name);
+ // logE(TAG, "miAudio: " + miAudio);
+ }
+ }
+ );
+ }
+ }
+ } catch (ClassNotFoundException e) {
+ logE(TAG, e);
+ }
+ findAndHookMethod("com.miui.misound.MiSoundApplication", "onCreate",
+ new MethodHook() {
+ @Override
+ protected void after(MethodHookParam param) {
+ Application application = (Application) param.thisObject;
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
+ intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
+ application.registerReceiver(new Listener(), intentFilter);
+ }
+ }
+ );
+ // settings get global effect_implementer
+ }
+
+ private static String effectImplementer(Context context) {
+ return Settings.Global.getString(context.getContentResolver(), "effect_implementer");
+ }
+
+ private static Object getAudioEffect() {
+ Class> AudioEffect = XposedHelpers.findClassIfExists("android.media.audiofx.AudioEffect", ClassLoader.getSystemClassLoader());
+ if (AudioEffect == null) return null;
+ return getAudio(AudioEffect);
+ }
+
+ private static Object getMiSound() {
+ Class> MiSound = XposedHelpers.findClassIfExists("android.media.audiofx.MiSound", ClassLoader.getSystemClassLoader());
+ if (MiSound == null) return null;
+ return XposedHelpers.newInstance(MiSound, 1, 0);
+ }
+
+ private static boolean hasControl(Object o) {
+ return (boolean) XposedHelpers.callMethod(o, "hasControl");
+ }
+
+ private static boolean isEnable(Object o) {
+ return (boolean) XposedHelpers.callMethod(o, "getEnabled");
+ }
+
+ private static void setEnable(Object o, boolean value) {
+ XposedHelpers.callMethod(o, "setEnabled", value);
+ }
+
+ @NonNull
+ private static Object getAudio(Class> AudioEffect) {
+ // Class> DolbyAudioEffectHelper = findClassIfExists("com.android.server.audio.dolbyeffect.DolbyEffectController$DolbyAudioEffectHelper",
+ // ClassLoader.getSystemClassLoader());
+ // logE(TAG, "DolbyAudioEffectHelper: " + DolbyAudioEffectHelper);
+ // UUID dolby = (UUID) XposedHelpers.getStaticObjectField(
+ // DolbyAudioEffectHelper, "EFFECT_TYPE_DOLBY_AUDIO_PROCESSING");
+ UUID EFFECT_TYPE_NULL = (UUID) XposedHelpers.getStaticObjectField(AudioEffect, "EFFECT_TYPE_NULL");
+ UUID dolby;
+ if (uuid.isEmpty()) {
+ dolby = UUID.fromString("9d4921da-8225-4f29-aefa-39537a04bcaa");
+ } else {
+ dolby = UUID.fromString(uuid);
+ }
+ return XposedHelpers.newInstance(AudioEffect, EFFECT_TYPE_NULL, dolby, 0, 0);
+ }
+
+ public static class Listener extends BroadcastReceiver {
+ private static Object AudioEffect = null;
+ private static Object MiSound = null;
+ private static boolean lastDolby;
+ private static boolean lastMiui;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action != null) {
+ switch (action) {
+ case BluetoothDevice.ACTION_ACL_CONNECTED -> {
+ init();
+ lastDolby = setAudio(AudioEffect, miDolby);
+ lastMiui = setAudio(MiSound, miAudio);
+ String implementer = effectImplementer(context);
+ // logE(TAG, "A: " + AudioEffect + " d: " + miDolby + " M: " + MiSound + " a: " + miAudio
+ // + " co: " + hasControl(AudioEffect) + " co1: " + hasControl(MiSound) +
+ // " laD: " + lastDolby + " laM: " + lastMiui + " im: " + implementer);
+ if (implementer != null) {
+ if ("dolby".equals(implementer)) {
+ lastDolby = true;
+ lastMiui = false;
+ } else if ("misound".equals(implementer)) {
+ lastDolby = false;
+ lastMiui = true;
+ }
+ }
+ }
+ case BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
+ init();
+ recoveryAudio(AudioEffect, miDolby, lastDolby);
+ recoveryAudio(MiSound, miAudio, lastMiui);
+ // logE(TAG, "A: " + AudioEffect + " d: " + miDolby + " M: " + MiSound + " a: " + miAudio
+ // + " co: " + hasControl(AudioEffect) + " co1: " + hasControl(MiSound) +
+ // " laD: " + lastDolby + " laM: " + lastMiui);
+ }
+ }
+ }
+ }
+
+ private static boolean setAudio(Object audio, Object otherAudio) {
+ boolean last;
+ if (audio != null) {
+ if (hasControl(audio)) {
+ last = isEnable(audio);
+ setEnable(audio, false);
+ return last;
+ } else if (otherAudio != null) {
+ if (hasControl(otherAudio)) {
+ last = isEnable(otherAudio);
+ setEnable(otherAudio, false);
+ return last;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static void recoveryAudio(Object audio, Object otherAudio, boolean last) {
+ if (audio != null) {
+ if (last != isEnable(audio)) {
+ if (hasControl(audio)) {
+ setEnable(audio, last);
+ } else if (otherAudio != null) {
+ if (hasControl(otherAudio)) {
+ setEnable(otherAudio, last);
+ }
+ }
+ }
+ }
+ }
+
+ private void init() {
+ if (AudioEffect == null) {
+ AudioEffect = getAudioEffect();
+ }
+ if (MiSound == null) {
+ MiSound = getMiSound();
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 3a8337fee1..2663e87823 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -1525,6 +1525,9 @@
禁止监控应用安装和删除
音质音效
+ 耳机状态自动切换原声
+ 音效效果 UUID 标识符
+ 杜比等音效标识符,无效果时再填写,自己想办法获取
提高音频采样率
小米设置
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 62cb893153..7d988a6aac 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1497,6 +1497,9 @@
Prohibit GetApps from monitoring app installation and uninstallation
Earphones
+ The headset status automatically switches to the original sound
+ Sound effect UUID identifier
+ Dolby and other sound identifiers, fill in when there is no effect, and find a way to get it yourself
Increase sampling rate
Mi Settings
diff --git a/app/src/main/res/xml/misound.xml b/app/src/main/res/xml/misound.xml
index b9c6ee0fcd..3def0f867e 100644
--- a/app/src/main/res/xml/misound.xml
+++ b/app/src/main/res/xml/misound.xml
@@ -3,8 +3,19 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
app:myLocation="@string/misound">
+
+
+
+
+ android:title="@string/misound_increase_sampling_rate" />
\ No newline at end of file