diff --git a/CodecMod/Readme.md b/CodecMod/Readme.md new file mode 100644 index 0000000..1beccc7 --- /dev/null +++ b/CodecMod/Readme.md @@ -0,0 +1,5 @@ +# CodecMod + +This Module allows you to selectively disable audio/video hardware/software encoders/decoders. + +Supports all codecs reported by Android through the MediaCodecList API. diff --git a/CodecMod/build.gradle.kts b/CodecMod/build.gradle.kts new file mode 100644 index 0000000..c99cfe2 --- /dev/null +++ b/CodecMod/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(libs.plugins.buildlogic.android.application) +} + +android { + namespace = "com.programminghoch10.CodecMod" + + defaultConfig { + minSdk = 16 + targetSdk = 35 + } + compileOptions { + isCoreLibraryDesugaringEnabled = true + } +} + +dependencies { + implementation(libs.androidx.preference) + coreLibraryDesugaring(libs.android.desugarJdkLibs) +} diff --git a/CodecMod/src/main/AndroidManifest.xml b/CodecMod/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b33a41b --- /dev/null +++ b/CodecMod/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/CodecMod/src/main/assets/xposed_init b/CodecMod/src/main/assets/xposed_init new file mode 100644 index 0000000..696169f --- /dev/null +++ b/CodecMod/src/main/assets/xposed_init @@ -0,0 +1 @@ +com.programminghoch10.CodecMod.Hook diff --git a/CodecMod/src/main/java/com/programminghoch10/CodecMod/CodecStore.java b/CodecMod/src/main/java/com/programminghoch10/CodecMod/CodecStore.java new file mode 100644 index 0000000..a1d86c2 --- /dev/null +++ b/CodecMod/src/main/java/com/programminghoch10/CodecMod/CodecStore.java @@ -0,0 +1,80 @@ +package com.programminghoch10.CodecMod; + +import static android.content.Context.MODE_WORLD_READABLE; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; + +import java.util.LinkedList; +import java.util.List; + +import de.robv.android.xposed.XSharedPreferences; + +public class CodecStore { + static final boolean DEFAULT_VALUE = true; + private static final boolean REMOVE_DEFAULT_VALUE_FROM_CONFIG = true; + private static final String PREFERENCES = "codecs"; + SharedPreferences sharedPreferences; + List receivers = new LinkedList<>(); + + @SuppressLint("WorldReadableFiles") + CodecStore(Context context) { + this.sharedPreferences = context.getSharedPreferences(PREFERENCES, MODE_WORLD_READABLE); + } + + CodecStore() { + this.sharedPreferences = new XSharedPreferences(BuildConfig.APPLICATION_ID, PREFERENCES); + } + + static String getKey(MediaCodecInfoWrapper mediaCodecInfo) { + return "codec_" + mediaCodecInfo.getCanonicalName(); + } + + boolean getCodecPreference(MediaCodecInfoWrapper mediaCodecInfo) { + return sharedPreferences.getBoolean(getKey(mediaCodecInfo), DEFAULT_VALUE); + } + + boolean setCodecPreference(MediaCodecInfoWrapper mediaCodecInfo, boolean enabled) { + boolean success; + if (REMOVE_DEFAULT_VALUE_FROM_CONFIG && enabled == DEFAULT_VALUE) { + success = sharedPreferences.edit().remove(getKey(mediaCodecInfo)).commit(); + } else { + success = sharedPreferences.edit().putBoolean(getKey(mediaCodecInfo), enabled).commit(); + } + if (!success) + return false; + dispatchOnCodecPreferenceChanged(mediaCodecInfo, enabled); + return true; + } + + void registerOnCodecPreferenceChangedListener(MediaCodecInfoWrapper mediaCodecInfo, OnCodecPreferenceChangedListener onCodecPreferenceChangedListener) { + OnCodecPreferenceChangedListenerMeta listener = new OnCodecPreferenceChangedListenerMeta(); + listener.mediaCodecInfo = mediaCodecInfo; + listener.callback = onCodecPreferenceChangedListener; + receivers.add(listener); + } + + private void dispatchOnCodecPreferenceChanged(MediaCodecInfoWrapper mediaCodecInfo, boolean enabled) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + receivers.stream() + .filter(r -> getKey(r.mediaCodecInfo).equals(getKey(mediaCodecInfo))) + .forEach(r -> r.callback.onCodecPreferenceChanged(enabled)); + } else { + for (OnCodecPreferenceChangedListenerMeta receiver : receivers) { + if (getKey(receiver.mediaCodecInfo).equals(getKey(mediaCodecInfo))) + receiver.callback.onCodecPreferenceChanged(enabled); + } + } + } + + interface OnCodecPreferenceChangedListener { + void onCodecPreferenceChanged(boolean value); + } + + private static class OnCodecPreferenceChangedListenerMeta { + MediaCodecInfoWrapper mediaCodecInfo; + OnCodecPreferenceChangedListener callback; + } +} diff --git a/CodecMod/src/main/java/com/programminghoch10/CodecMod/Hook.java b/CodecMod/src/main/java/com/programminghoch10/CodecMod/Hook.java new file mode 100644 index 0000000..a3df051 --- /dev/null +++ b/CodecMod/src/main/java/com/programminghoch10/CodecMod/Hook.java @@ -0,0 +1,89 @@ +package com.programminghoch10.CodecMod; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.os.Build; + +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import de.robv.android.xposed.IXposedHookLoadPackage; +import de.robv.android.xposed.XC_MethodReplacement; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +public class Hook implements IXposedHookLoadPackage { + + MediaCodecInfo[] getFilteredMediaCodecInfos(MediaCodecInfo[] unfilteredMediaCodecInfos) { + CodecStore codecStore = new CodecStore(); + return Arrays.stream(unfilteredMediaCodecInfos) + .map(MediaCodecInfoWrapper::new) + .filter(codecStore::getCodecPreference) + .map(MediaCodecInfoWrapper::getOriginalMediaCodecInfo) + .toArray(MediaCodecInfo[]::new); + } + + // helper function, only to be used on mediaCodecs = new LinkedList<>(); + final int codecCount = (int) XposedBridge.invokeOriginalMethod(XposedHelpers.findMethodExact(MediaCodecList.class, "getCodecCount"), null, null); + for (int i = 0; i < codecCount; i++) + mediaCodecs.add((MediaCodecInfo) XposedBridge.invokeOriginalMethod(XposedHelpers.findMethodExact(MediaCodecList.class, "getCodecInfoAt"), null, new Object[]{i})); + return getFilteredMediaCodecInfos(mediaCodecs.toArray(MediaCodecInfo[]::new)); + } + + @Override + public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { + if (lpparam.packageName.equals(BuildConfig.APPLICATION_ID)) return; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + XposedHelpers.findAndHookMethod(MediaCodecList.class, "getCodecInfos", new XC_MethodReplacement() { + @Override + protected Object replaceHookedMethod(MethodHookParam param) throws Throwable { + MediaCodecInfo[] mediaCodecInfos = (MediaCodecInfo[]) XposedBridge.invokeOriginalMethod(param.method, param.thisObject, param.args); + if (mediaCodecInfos.length == 0) return mediaCodecInfos; + return getFilteredMediaCodecInfos(mediaCodecInfos); + } + }); + + // reimplementations of deprecated methods for compatibility + XposedHelpers.findAndHookMethod(MediaCodecList.class, "getCodecCount", new XC_MethodReplacement() { + @Override + protected Object replaceHookedMethod(MethodHookParam param) throws Throwable { + return (new MediaCodecList(MediaCodecList.REGULAR_CODECS)).getCodecInfos().length; + } + }); + XposedHelpers.findAndHookMethod(MediaCodecList.class, "getCodecInfoAt", int.class, new XC_MethodReplacement() { + @Override + protected Object replaceHookedMethod(MethodHookParam param) throws Throwable { + final int position = (int) param.args[0]; + MediaCodecInfo[] mediaCodecInfos = (new MediaCodecList(MediaCodecList.REGULAR_CODECS)).getCodecInfos(); + if (position < 0 || position >= mediaCodecInfos.length) throw new IllegalArgumentException(); + return mediaCodecInfos[position]; + } + }); + } else { + XposedHelpers.findAndHookMethod(MediaCodecList.class, "getCodecCount", new XC_MethodReplacement() { + @Override + protected Object replaceHookedMethod(MethodHookParam param) throws Throwable { + return getFilteredMediaCodecInfos().length; + } + }); + XposedHelpers.findAndHookMethod(MediaCodecList.class, "getCodecInfoAt", int.class, new XC_MethodReplacement() { + @Override + protected Object replaceHookedMethod(MethodHookParam param) throws Throwable { + final int position = (int) param.args[0]; + MediaCodecInfo[] mediaCodecInfos = getFilteredMediaCodecInfos(); + if (position < 0 || position >= mediaCodecInfos.length) throw new IllegalArgumentException(); + return mediaCodecInfos[position]; + } + }); + } + } +} diff --git a/CodecMod/src/main/java/com/programminghoch10/CodecMod/MediaCodecInfoWrapper.java b/CodecMod/src/main/java/com/programminghoch10/CodecMod/MediaCodecInfoWrapper.java new file mode 100644 index 0000000..29c340b --- /dev/null +++ b/CodecMod/src/main/java/com/programminghoch10/CodecMod/MediaCodecInfoWrapper.java @@ -0,0 +1,69 @@ +package com.programminghoch10.CodecMod; + +import android.media.MediaCodecInfo; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +/** + * drop in replacement for MediaCodecInfo + * with compatibility checks for older SDKs + * + * @see MediaCodecInfo + */ +public class MediaCodecInfoWrapper { + private final MediaCodecInfo mediaCodecInfo; + + MediaCodecInfoWrapper(MediaCodecInfo mediaCodecInfo) { + this.mediaCodecInfo = mediaCodecInfo; + } + + public MediaCodecInfo getOriginalMediaCodecInfo() { + return mediaCodecInfo; + } + + public String getCanonicalName() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return mediaCodecInfo.getCanonicalName(); + } + return mediaCodecInfo.getName(); + } + + public boolean isAlias() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return mediaCodecInfo.isAlias(); + } + return false; + } + + public String getName() { + return mediaCodecInfo.getName(); + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + public boolean isHardwareAccelerated() { + return mediaCodecInfo.isHardwareAccelerated(); + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + public boolean isSoftwareOnly() { + return mediaCodecInfo.isSoftwareOnly(); + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + public boolean isVendor() { + return mediaCodecInfo.isVendor(); + } + + public boolean isEncoder() { + return mediaCodecInfo.isEncoder(); + } + + public boolean isDecoder() { + return !isEncoder(); + } + + public String[] getSupportedTypes() { + return mediaCodecInfo.getSupportedTypes(); + } +} diff --git a/CodecMod/src/main/java/com/programminghoch10/CodecMod/SettingsActivity.java b/CodecMod/src/main/java/com/programminghoch10/CodecMod/SettingsActivity.java new file mode 100644 index 0000000..589bff8 --- /dev/null +++ b/CodecMod/src/main/java/com/programminghoch10/CodecMod/SettingsActivity.java @@ -0,0 +1,91 @@ +package com.programminghoch10.CodecMod; + +import android.app.ActionBar; +import android.media.MediaCodecList; +import android.os.Build; +import android.os.Bundle; + +import androidx.fragment.app.FragmentActivity; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.SwitchPreference; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +public class SettingsActivity extends FragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.settings_activity); + if (savedInstanceState == null) { + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.settings, new SettingsFragment()) + .commit(); + } + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled( + getSupportFragmentManager().getBackStackEntryCount() > 0 + ); + } + } + + public static class SettingsFragment extends PreferenceFragmentCompat { + private static final boolean SHOW_ALIASES = true; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.root_preferences, rootKey); + //getPreferenceManager().setSharedPreferencesName("codecs"); + CodecStore codecStore = new CodecStore(requireContext()); + PreferenceCategory decodersPreferenceCategory = findPreference("category_decoders"); + PreferenceCategory encodersPreferenceCategory = findPreference("category_encoders"); + + List mediaCodecs; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + mediaCodecs = Arrays.stream(mediaCodecList.getCodecInfos()) + .map(MediaCodecInfoWrapper::new) + .toList(); + } else { + mediaCodecs = new LinkedList<>(); + for (int i = 0; i < MediaCodecList.getCodecCount(); i++) + mediaCodecs.add(new MediaCodecInfoWrapper(MediaCodecList.getCodecInfoAt(i))); + } + for (MediaCodecInfoWrapper mediaCodecInfo : mediaCodecs) { + if (mediaCodecInfo.isAlias() && !SHOW_ALIASES) continue; + SwitchPreference preference = new SwitchPreference(requireContext()); + preference.setPersistent(false); + preference.setDefaultValue(CodecStore.DEFAULT_VALUE); + preference.setKey(CodecStore.getKey(mediaCodecInfo)); + preference.setOnPreferenceChangeListener((p, n) -> codecStore.setCodecPreference(mediaCodecInfo, (Boolean) n)); + codecStore.registerOnCodecPreferenceChangedListener(mediaCodecInfo, value -> { + if (preference.isChecked() != value) preference.setChecked(value); + }); + preference.setTitle(mediaCodecInfo.getName() + + (mediaCodecInfo.getName().equals(mediaCodecInfo.getCanonicalName()) ? "" : " (" + mediaCodecInfo.getCanonicalName() + ")")); + StringBuilder summaryBuilder = new StringBuilder(); + summaryBuilder.append(String.format(getString(R.string.supported_types), Arrays.toString(mediaCodecInfo.getSupportedTypes()))); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + summaryBuilder.append("\n"); + summaryBuilder.append(String.format(getString(R.string.hardware_accelerated), mediaCodecInfo.isHardwareAccelerated())); + summaryBuilder.append("\n"); + summaryBuilder.append(String.format(getString(R.string.software_only), mediaCodecInfo.isSoftwareOnly())); + if (SHOW_ALIASES) { + summaryBuilder.append("\n"); + summaryBuilder.append(String.format(getString(R.string.alias), mediaCodecInfo.isAlias())); + } + summaryBuilder.append("\n"); + summaryBuilder.append(String.format(getString(R.string.vendor), mediaCodecInfo.isVendor())); + } + preference.setSummary(summaryBuilder); + PreferenceCategory preferenceCategory = mediaCodecInfo.isEncoder() ? encodersPreferenceCategory : decodersPreferenceCategory; + preferenceCategory.addPreference(preference); + preference.setChecked(codecStore.getCodecPreference(mediaCodecInfo)); + } + } + } +} diff --git a/CodecMod/src/main/res/layout/settings_activity.xml b/CodecMod/src/main/res/layout/settings_activity.xml new file mode 100644 index 0000000..89fbea7 --- /dev/null +++ b/CodecMod/src/main/res/layout/settings_activity.xml @@ -0,0 +1,12 @@ + + + + diff --git a/CodecMod/src/main/res/values-v21/themes.xml b/CodecMod/src/main/res/values-v21/themes.xml new file mode 100644 index 0000000..6ee0359 --- /dev/null +++ b/CodecMod/src/main/res/values-v21/themes.xml @@ -0,0 +1,4 @@ + + + + diff --git a/CodecMod/src/main/res/values/strings.xml b/CodecMod/src/main/res/values/strings.xml new file mode 100644 index 0000000..ec4ae55 --- /dev/null +++ b/CodecMod/src/main/res/values/strings.xml @@ -0,0 +1,11 @@ + + CodecMod + This Module allows you to selectively disable audio/video hardware/software encoders/decoders. + + CodecMod Settings + Hardware-accelerated: %b + Software-only: %b + Supported MIME-Types: %s + Alias: %b + Vendor: %b + diff --git a/CodecMod/src/main/res/values/themes.xml b/CodecMod/src/main/res/values/themes.xml new file mode 100644 index 0000000..33bc3e6 --- /dev/null +++ b/CodecMod/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +