From 7baf2b56653e7883f6bad1ae88f80fd2f504e696 Mon Sep 17 00:00:00 2001 From: "openai-code-agent[bot]" <242516109+codex@users.noreply.github.com> Date: Sat, 21 Feb 2026 10:25:14 +0800 Subject: [PATCH] feat(settings): support TMoe entry on Telegram 12.4 while keeping legacy compatibility Telegram 12.4 migrated the settings UI from ProfileActivity rows to SettingsActivity (UniversalRecyclerView/UItem), which broke the old TMoe settings injection path. This change adds a dual-path hook strategy: - keep the existing ProfileActivity row hook for older Telegram versions - add SettingsActivity hooks for 12.4+: - inject a TMoe settings item before the Language item in fillItems() - handle TMoe item click in onClick() and open SettingsFragment - add duplicate-insertion guards and safe fallback when SettingsActivity is absent --- .../tmoe/hook/core/SettingEntryHook.java | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/app/src/main/java/cc/ioctl/tmoe/hook/core/SettingEntryHook.java b/app/src/main/java/cc/ioctl/tmoe/hook/core/SettingEntryHook.java index 5d73ce77..36b7b2de 100644 --- a/app/src/main/java/cc/ioctl/tmoe/hook/core/SettingEntryHook.java +++ b/app/src/main/java/cc/ioctl/tmoe/hook/core/SettingEntryHook.java @@ -5,14 +5,20 @@ import androidx.annotation.NonNull; +import java.lang.reflect.Method; +import java.util.ArrayList; + import cc.ioctl.tmoe.R; import cc.ioctl.tmoe.fragment.SettingsFragment; import cc.ioctl.tmoe.lifecycle.Parasitics; import cc.ioctl.tmoe.rtti.ProxyFragmentRttiHandler; import cc.ioctl.tmoe.ui.LocaleController; import cc.ioctl.tmoe.util.HostInfo; +import cc.ioctl.tmoe.util.Initiator; import cc.ioctl.tmoe.util.Reflex; import cc.ioctl.tmoe.util.Utils; +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; public class SettingEntryHook implements Initializable, ProfileActivityRowHook.Callback { public static final SettingEntryHook INSTANCE = new SettingEntryHook(); @@ -21,6 +27,12 @@ private SettingEntryHook() { } private static final String TMOE_SETTINGS_ROW = "TMOE_SETTINGS_ROW"; + private static final int TMOE_SETTINGS_ITEM_ID = 0x7F0E0001; + private static final int SETTINGS_ACTIVITY_LANGUAGE_ITEM_ID = 10; + private static final int SETTINGS_ACTIVITY_ICON_COLOR_TOP = 0xFFB07AF5; + private static final int SETTINGS_ACTIVITY_ICON_COLOR_BOTTOM = 0xFF8C57E8; + private static Method sSettingsCellFactoryMethod = null; + private static boolean sSettingsActivityHooked = false; private boolean mInitialized = false; @@ -30,6 +42,7 @@ public boolean initialize() { return true; } ProfileActivityRowHook.addCallback(this); + hookSettingsActivityEntry(); mInitialized = true; return true; } @@ -99,4 +112,126 @@ public void onInsertRow(@NonNull ProfileActivityRowHook.RowManipulator manipulat } manipulator.insertRowAtPosition(TMOE_SETTINGS_ROW, targetRow); } + + private static void hookSettingsActivityEntry() { + if (sSettingsActivityHooked) { + return; + } + try { + Class kSettingsActivity = Initiator.load("org.telegram.ui.SettingsActivity"); + if (kSettingsActivity == null) { + // old versions do not have SettingsActivity, keep legacy ProfileActivity hook only + return; + } + Class kSettingCellFactory = Initiator.load("org.telegram.ui.SettingsActivity$SettingCell$Factory"); + if (kSettingCellFactory == null) { + Utils.loge("unable to find SettingsActivity$SettingCell$Factory"); + return; + } + sSettingsCellFactoryMethod = kSettingCellFactory.getDeclaredMethod( + "of", + int.class, int.class, int.class, int.class, CharSequence.class, CharSequence.class, CharSequence.class + ); + sSettingsCellFactoryMethod.setAccessible(true); + + Method fillItems = null; + Method onClick = null; + for (Method m : kSettingsActivity.getDeclaredMethods()) { + if (fillItems == null && "fillItems".equals(m.getName()) + && m.getParameterTypes().length == 2 + && ArrayList.class.isAssignableFrom(m.getParameterTypes()[0])) { + fillItems = m; + } else if (onClick == null && "onClick".equals(m.getName()) + && m.getParameterTypes().length == 5) { + onClick = m; + } + } + if (fillItems == null || onClick == null) { + Utils.loge("unable to find SettingsActivity#fillItems or #onClick"); + return; + } + + XposedBridge.hookMethod(fillItems, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + if (!(param.args[0] instanceof ArrayList)) { + return; + } + @SuppressWarnings("unchecked") + ArrayList items = (ArrayList) param.args[0]; + if (items.isEmpty()) { + return; + } + int insertAt = findSettingsActivityLanguageItemIndex(items); + if (insertAt < 0 || containsSettingsActivityItem(items, TMOE_SETTINGS_ITEM_ID)) { + return; + } + Object item = createSettingsActivityItem(); + if (item != null) { + items.add(insertAt, item); + } + } + }); + + XposedBridge.hookMethod(onClick, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + Object item = param.args[0]; + if (getSettingsActivityItemId(item) == TMOE_SETTINGS_ITEM_ID) { + presentTMoeSettingsFragment(param.thisObject); + param.setResult(null); + } + } + }); + sSettingsActivityHooked = true; + } catch (Throwable e) { + Utils.loge(e); + } + } + + private static Object createSettingsActivityItem() { + if (sSettingsCellFactoryMethod == null) { + return null; + } + try { + Parasitics.injectModuleResources(HostInfo.getApplication().getResources()); + String text = LocaleController.getString("TMoeSettings", R.string.TMoeSettings); + return sSettingsCellFactoryMethod.invoke( + null, + TMOE_SETTINGS_ITEM_ID, + SETTINGS_ACTIVITY_ICON_COLOR_TOP, + SETTINGS_ACTIVITY_ICON_COLOR_BOTTOM, + R.drawable.ic_setting_hex_outline_24, + text, + null, + null + ); + } catch (Throwable e) { + Utils.loge(e); + return null; + } + } + + private static int findSettingsActivityLanguageItemIndex(@NonNull ArrayList items) { + for (int i = 0; i < items.size(); i++) { + if (getSettingsActivityItemId(items.get(i)) == SETTINGS_ACTIVITY_LANGUAGE_ITEM_ID) { + return i; + } + } + return -1; + } + + private static boolean containsSettingsActivityItem(@NonNull ArrayList items, int itemId) { + for (Object item : items) { + if (getSettingsActivityItemId(item) == itemId) { + return true; + } + } + return false; + } + + private static int getSettingsActivityItemId(Object item) { + Object id = item == null ? null : Reflex.getInstanceObjectOrNull(item, "id"); + return id instanceof Number ? ((Number) id).intValue() : Integer.MIN_VALUE; + } }