Skip to content

Commit 7baf2b5

Browse files
Codexchsbuffer
authored andcommitted
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
1 parent d415f43 commit 7baf2b5

1 file changed

Lines changed: 135 additions & 0 deletions

File tree

app/src/main/java/cc/ioctl/tmoe/hook/core/SettingEntryHook.java

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,20 @@
55

66
import androidx.annotation.NonNull;
77

8+
import java.lang.reflect.Method;
9+
import java.util.ArrayList;
10+
811
import cc.ioctl.tmoe.R;
912
import cc.ioctl.tmoe.fragment.SettingsFragment;
1013
import cc.ioctl.tmoe.lifecycle.Parasitics;
1114
import cc.ioctl.tmoe.rtti.ProxyFragmentRttiHandler;
1215
import cc.ioctl.tmoe.ui.LocaleController;
1316
import cc.ioctl.tmoe.util.HostInfo;
17+
import cc.ioctl.tmoe.util.Initiator;
1418
import cc.ioctl.tmoe.util.Reflex;
1519
import cc.ioctl.tmoe.util.Utils;
20+
import de.robv.android.xposed.XC_MethodHook;
21+
import de.robv.android.xposed.XposedBridge;
1622

1723
public class SettingEntryHook implements Initializable, ProfileActivityRowHook.Callback {
1824
public static final SettingEntryHook INSTANCE = new SettingEntryHook();
@@ -21,6 +27,12 @@ private SettingEntryHook() {
2127
}
2228

2329
private static final String TMOE_SETTINGS_ROW = "TMOE_SETTINGS_ROW";
30+
private static final int TMOE_SETTINGS_ITEM_ID = 0x7F0E0001;
31+
private static final int SETTINGS_ACTIVITY_LANGUAGE_ITEM_ID = 10;
32+
private static final int SETTINGS_ACTIVITY_ICON_COLOR_TOP = 0xFFB07AF5;
33+
private static final int SETTINGS_ACTIVITY_ICON_COLOR_BOTTOM = 0xFF8C57E8;
34+
private static Method sSettingsCellFactoryMethod = null;
35+
private static boolean sSettingsActivityHooked = false;
2436

2537
private boolean mInitialized = false;
2638

@@ -30,6 +42,7 @@ public boolean initialize() {
3042
return true;
3143
}
3244
ProfileActivityRowHook.addCallback(this);
45+
hookSettingsActivityEntry();
3346
mInitialized = true;
3447
return true;
3548
}
@@ -99,4 +112,126 @@ public void onInsertRow(@NonNull ProfileActivityRowHook.RowManipulator manipulat
99112
}
100113
manipulator.insertRowAtPosition(TMOE_SETTINGS_ROW, targetRow);
101114
}
115+
116+
private static void hookSettingsActivityEntry() {
117+
if (sSettingsActivityHooked) {
118+
return;
119+
}
120+
try {
121+
Class<?> kSettingsActivity = Initiator.load("org.telegram.ui.SettingsActivity");
122+
if (kSettingsActivity == null) {
123+
// old versions do not have SettingsActivity, keep legacy ProfileActivity hook only
124+
return;
125+
}
126+
Class<?> kSettingCellFactory = Initiator.load("org.telegram.ui.SettingsActivity$SettingCell$Factory");
127+
if (kSettingCellFactory == null) {
128+
Utils.loge("unable to find SettingsActivity$SettingCell$Factory");
129+
return;
130+
}
131+
sSettingsCellFactoryMethod = kSettingCellFactory.getDeclaredMethod(
132+
"of",
133+
int.class, int.class, int.class, int.class, CharSequence.class, CharSequence.class, CharSequence.class
134+
);
135+
sSettingsCellFactoryMethod.setAccessible(true);
136+
137+
Method fillItems = null;
138+
Method onClick = null;
139+
for (Method m : kSettingsActivity.getDeclaredMethods()) {
140+
if (fillItems == null && "fillItems".equals(m.getName())
141+
&& m.getParameterTypes().length == 2
142+
&& ArrayList.class.isAssignableFrom(m.getParameterTypes()[0])) {
143+
fillItems = m;
144+
} else if (onClick == null && "onClick".equals(m.getName())
145+
&& m.getParameterTypes().length == 5) {
146+
onClick = m;
147+
}
148+
}
149+
if (fillItems == null || onClick == null) {
150+
Utils.loge("unable to find SettingsActivity#fillItems or #onClick");
151+
return;
152+
}
153+
154+
XposedBridge.hookMethod(fillItems, new XC_MethodHook() {
155+
@Override
156+
protected void afterHookedMethod(MethodHookParam param) {
157+
if (!(param.args[0] instanceof ArrayList)) {
158+
return;
159+
}
160+
@SuppressWarnings("unchecked")
161+
ArrayList<Object> items = (ArrayList<Object>) param.args[0];
162+
if (items.isEmpty()) {
163+
return;
164+
}
165+
int insertAt = findSettingsActivityLanguageItemIndex(items);
166+
if (insertAt < 0 || containsSettingsActivityItem(items, TMOE_SETTINGS_ITEM_ID)) {
167+
return;
168+
}
169+
Object item = createSettingsActivityItem();
170+
if (item != null) {
171+
items.add(insertAt, item);
172+
}
173+
}
174+
});
175+
176+
XposedBridge.hookMethod(onClick, new XC_MethodHook() {
177+
@Override
178+
protected void beforeHookedMethod(MethodHookParam param) {
179+
Object item = param.args[0];
180+
if (getSettingsActivityItemId(item) == TMOE_SETTINGS_ITEM_ID) {
181+
presentTMoeSettingsFragment(param.thisObject);
182+
param.setResult(null);
183+
}
184+
}
185+
});
186+
sSettingsActivityHooked = true;
187+
} catch (Throwable e) {
188+
Utils.loge(e);
189+
}
190+
}
191+
192+
private static Object createSettingsActivityItem() {
193+
if (sSettingsCellFactoryMethod == null) {
194+
return null;
195+
}
196+
try {
197+
Parasitics.injectModuleResources(HostInfo.getApplication().getResources());
198+
String text = LocaleController.getString("TMoeSettings", R.string.TMoeSettings);
199+
return sSettingsCellFactoryMethod.invoke(
200+
null,
201+
TMOE_SETTINGS_ITEM_ID,
202+
SETTINGS_ACTIVITY_ICON_COLOR_TOP,
203+
SETTINGS_ACTIVITY_ICON_COLOR_BOTTOM,
204+
R.drawable.ic_setting_hex_outline_24,
205+
text,
206+
null,
207+
null
208+
);
209+
} catch (Throwable e) {
210+
Utils.loge(e);
211+
return null;
212+
}
213+
}
214+
215+
private static int findSettingsActivityLanguageItemIndex(@NonNull ArrayList<?> items) {
216+
for (int i = 0; i < items.size(); i++) {
217+
if (getSettingsActivityItemId(items.get(i)) == SETTINGS_ACTIVITY_LANGUAGE_ITEM_ID) {
218+
return i;
219+
}
220+
}
221+
return -1;
222+
}
223+
224+
private static boolean containsSettingsActivityItem(@NonNull ArrayList<?> items, int itemId) {
225+
for (Object item : items) {
226+
if (getSettingsActivityItemId(item) == itemId) {
227+
return true;
228+
}
229+
}
230+
return false;
231+
}
232+
233+
private static int getSettingsActivityItemId(Object item) {
234+
Object id = item == null ? null : Reflex.getInstanceObjectOrNull(item, "id");
235+
return id instanceof Number ? ((Number) id).intValue() : Integer.MIN_VALUE;
236+
}
102237
}

0 commit comments

Comments
 (0)