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