Skip to content

Commit c83357c

Browse files
committed
feat: patch overhaul #1
1 parent e7c0448 commit c83357c

2 files changed

Lines changed: 263 additions & 45 deletions

File tree

Lines changed: 261 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,291 @@
11
package app.revanced.extension.customfilters;
22

33
import android.content.Context;
4-
import android.os.Handler;
5-
import android.os.Looper;
6-
import android.util.Log;
74
import android.widget.Toast;
8-
95
import java.lang.reflect.Constructor;
6+
import java.lang.reflect.Field;
7+
import java.lang.reflect.InvocationHandler;
108
import java.lang.reflect.Method;
9+
import java.lang.reflect.Proxy;
1110

11+
/**
12+
* Reflection-only helper that creates a "Custom Tint" slot in FigureFiltersToolTable.
13+
*
14+
* Call: CustomTintSlotHook.installCustomTintSlot(tableObject);
15+
*/
1216
public class TintFieldHook {
13-
public static void installTintField(Object table) {
17+
public static void installCustomTintSlot(Object table) {
1418
try {
15-
Log.d("ReVancedCustomFilter", "installTintField called!");
16-
1719
ClassLoader cl = table.getClass().getClassLoader();
1820

19-
// Get FigureFiltersToolTable module + context
20-
Class<?> figureFiltersClass = Class.forName(
21-
"org.fortheloss.sticknodes.animationscreen.modules.tooltables.FigureFiltersToolTable",
22-
false,
23-
cl
24-
);
21+
// Core classes
22+
Class<?> figureFiltersClass = Class.forName("org.fortheloss.sticknodes.animationscreen.modules.tooltables.FigureFiltersToolTable", false, cl);
23+
Class<?> labelColorInputClass = Class.forName("org.fortheloss.framework.LabelColorInputIncrementField", false, cl);
24+
Class<?> animationScreenClass = Class.forName("org.fortheloss.sticknodes.animationscreen.AnimationScreen", false, cl);
25+
Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", false, cl);
26+
Class<?> tableClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Table", false, cl);
27+
Class<?> cellClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Cell", false, cl);
28+
Class<?> moduleClass = Class.forName("org.fortheloss.sticknodes.animationscreen.modules.Module", false, cl);
29+
30+
// Get module/context
2531
Method getModule = figureFiltersClass.getMethod("getModule");
2632
Object module = getModule.invoke(table);
2733
Method getContext = module.getClass().getMethod("getContext");
28-
Context context = (Context) getContext.invoke(module);
29-
30-
// ✅ Show a Toast to confirm the hook actually ran
31-
new Handler(Looper.getMainLooper()).post(() ->
32-
Toast.makeText(context, "ReVanced filter patch active!", Toast.LENGTH_LONG).show()
33-
);
34+
Object animationScreen = getContext.invoke(module); // AnimationScreen instance, used by ctor
3435

35-
// Continue with your original code
36+
// Localization helper: App.localize("tint")
3637
Class<?> appClass = Class.forName("org.fortheloss.sticknodes.App", false, cl);
37-
Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", false, cl);
38-
Class<?> labelFieldClass = Class.forName("org.fortheloss.framework.LabelColorInputIncrementField", false, cl);
38+
Method localize = appClass.getMethod("localize", String.class);
39+
String labelText = (String) localize.invoke(null, "custom"); // use "custom" key; replace if you want "tint"
3940

40-
Constructor<?> ctor = labelFieldClass.getConstructor(
41-
Context.class,
42-
String.class, String.class, int.class,
43-
float.class, float.class, boolean.class
41+
// Constructor for LabelColorInputIncrementField
42+
// Smali showed signature: <init>(AnimationScreen, String, String, I, F, F, Z)
43+
Constructor<?> ctor = labelColorInputClass.getDeclaredConstructor(
44+
animationScreenClass,
45+
String.class,
46+
String.class,
47+
int.class,
48+
float.class,
49+
float.class,
50+
boolean.class
4451
);
52+
ctor.setAccessible(true);
4553

46-
Method localize = appClass.getMethod("localize", String.class);
47-
String tintLabel = (String) localize.invoke(null, "tint");
48-
49-
Object field = ctor.newInstance(context, tintLabel, "0.00", 4, 0.0f, 1.0f, true);
54+
// Create instance
55+
// Arguments: (AnimationScreen, label text, defaultString, precision/int, min, max, boolean)
56+
Object fieldInstance = ctor.newInstance(animationScreen, labelText, "0.00", 4, 0.0f, 1.0f, true);
5057

58+
// registerWidget(Actor, int)
5159
Method registerWidget = figureFiltersClass.getMethod("registerWidget", Object.class, int.class);
52-
registerWidget.invoke(table, field, 107);
60+
// Choose ID 108 (0x6C) — original tint used 0x6B = 107
61+
registerWidget.invoke(table, fieldInstance, 108);
62+
63+
// setHighFidelity(true)
64+
Method setHigh = labelColorInputClass.getMethod("setHighFidelity", boolean.class);
65+
setHigh.invoke(fieldInstance, true);
66+
67+
// setValue(Color.WHITE)
68+
Field whiteField = colorClass.getField("WHITE");
69+
Object whiteColor = whiteField.get(null);
70+
Method setValue = labelColorInputClass.getMethod("setValue", colorClass);
71+
setValue.invoke(fieldInstance, whiteColor);
72+
73+
// Add to mFiltersSubTable: get field mFiltersSubTable from the FigureFiltersToolTable
74+
Field filtersSubTableField = figureFiltersClass.getDeclaredField("mFiltersSubTable");
75+
filtersSubTableField.setAccessible(true);
76+
Object filtersSubTable = filtersSubTableField.get(table); // Table instance
77+
78+
// Table.add(Actor) --> returns Cell
79+
Method tableAdd = tableClass.getMethod("add", Object.class);
80+
Object cell = tableAdd.invoke(filtersSubTable, fieldInstance);
5381

54-
Method setHighFidelity = labelFieldClass.getMethod("setHighFidelity", boolean.class);
55-
setHighFidelity.invoke(field, true);
82+
// Call colspan(2) and fillX() on returned Cell
83+
try {
84+
Method colspan = cellClass.getMethod("colspan", int.class);
85+
Object cellAfterColspan = colspan.invoke(cell, 2);
5686

57-
Object whiteColor = colorClass.getField("WHITE").get(null);
58-
Method setValue = labelFieldClass.getMethod("setValue", colorClass);
59-
setValue.invoke(field, whiteColor);
87+
Method fillX = cellClass.getMethod("fillX");
88+
Object cellAfterFill = fillX.invoke(cellAfterColspan);
6089

61-
Log.d("ReVancedCustomFilter", "installTintField completed successfully!");
90+
// optional: call height(float) if you want to match separator height (skip if you do not know region)
91+
// Method height = cellClass.getMethod("height", float.class);
92+
// height.invoke(cellAfterFill, someFloat);
93+
} catch (NoSuchMethodException nsme) {
94+
// Some Cell signatures are generified — try alternate names or ignore if unavailable
95+
}
96+
97+
// table.row()
98+
Method tableRow = tableClass.getMethod("row");
99+
tableRow.invoke(filtersSubTable);
100+
101+
// Create a dynamic proxy listener for LabelColorInputIncrementField$ColorFieldListener
102+
Class<?> listenerInterface = Class.forName("org.fortheloss.framework.LabelColorInputIncrementField$ColorFieldListener", false, cl);
103+
104+
Object listenerProxy = Proxy.newProxyInstance(
105+
cl,
106+
new Class<?>[]{listenerInterface},
107+
new InvocationHandler() {
108+
// caching reflective lookups for speed
109+
Method redrawModuleMethod = null;
110+
Field animationBasedModuleRefField = null;
111+
Method setAmountMethod = null;
112+
Method setColorMethod = null;
113+
114+
{
115+
try {
116+
// redrawModule() is a method on FigureFiltersToolTable
117+
redrawModuleMethod = figureFiltersClass.getMethod("redrawModule");
118+
} catch (Exception ignored) {}
119+
try {
120+
animationBasedModuleRefField = figureFiltersClass.getDeclaredField("mAnimationBasedModuleRef");
121+
animationBasedModuleRefField.setAccessible(true);
122+
// attempt to load methods on the animation-based module (if present)
123+
Class<?> animBasedModuleClass = animationBasedModuleRefField.getType();
124+
// Common method names (from app): setFigureTintAmountTo(float), setFigureTintColor(Color)
125+
try {
126+
setAmountMethod = animBasedModuleClass.getMethod("setFigureTintAmountTo", float.class);
127+
} catch (NoSuchMethodException ignored) {}
128+
try {
129+
setColorMethod = animBasedModuleClass.getMethod("setFigureTintColor", colorClass);
130+
} catch (NoSuchMethodException ignored) {}
131+
} catch (Exception ignored) {}
132+
}
133+
134+
@Override
135+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
136+
String name = method.getName();
137+
try {
138+
if ("onTextFieldTouchEvent".equals(name)) {
139+
// redrawModule()
140+
if (redrawModuleMethod != null) {
141+
redrawModuleMethod.invoke(table);
142+
}
143+
return null;
144+
} else if ("onTextFieldChange".equals(name)) {
145+
// args: (float value, boolean isFinal)
146+
float value = ((Number) args[0]).floatValue();
147+
if (redrawModuleMethod != null) {
148+
redrawModuleMethod.invoke(table);
149+
}
150+
if (animationBasedModuleRefField != null && setAmountMethod != null) {
151+
Object animRef = animationBasedModuleRefField.get(table);
152+
if (animRef != null) {
153+
setAmountMethod.invoke(animRef, value);
154+
}
155+
}
156+
return null;
157+
} else if ("onColorChange".equals(name)) {
158+
// args: (Color color)
159+
Object colorArg = args[0];
160+
if (animationBasedModuleRefField != null && setColorMethod != null && colorArg != null) {
161+
Object animRef = animationBasedModuleRefField.get(table);
162+
if (animRef != null) {
163+
setColorMethod.invoke(animRef, colorArg);
164+
}
165+
}
166+
return null;
167+
}
168+
} catch (Throwable t) {
169+
// swallow individual listener errors — but print for debugging
170+
t.printStackTrace();
171+
}
172+
return null;
173+
}
174+
}
175+
);
176+
177+
// setFieldListener(listener)
178+
Method setFieldListener = labelColorInputClass.getMethod("setFieldListener", Class.forName("org.fortheloss.framework.LabelInputIncrementField$FieldListener", false, cl));
179+
// Note: LabelColorInputIncrementField likely inherits/uses the nested FieldListener type; pass the proxy
180+
setFieldListener.invoke(fieldInstance, listenerProxy);
181+
182+
// Show a small Toast for debugging/confirmation (if possible)
183+
try {
184+
// module.getContext() earlier returned AnimationScreen, but it should be a Context or have an Activity reference
185+
// We'll attempt to use the animationScreen's getContext() result if it's an actual android Context
186+
Context ctx = null;
187+
if (animationScreen instanceof Context) {
188+
ctx = (Context) animationScreen;
189+
} else {
190+
// If AnimationScreen isn't a Context, attempt to retrieve an android context via module.getContext() or similar
191+
// Fallback: try module.getContext() result (we already used animationScreen variable)
192+
// If not a Context, this will fail silently
193+
}
194+
if (ctx != null) {
195+
Toast.makeText(ctx, "Custom filter slot installed", Toast.LENGTH_SHORT).show();
196+
} else {
197+
// If we didn't get an Android Context, attempt to call android.widget.Toast with reflection
198+
try {
199+
Class<?> toastClass = Class.forName("android.widget.Toast", false, cl);
200+
Method makeText = toastClass.getMethod("makeText", Context.class, CharSequence.class, int.class);
201+
// Attempt to reuse animationScreen as Context if it actually is; if not, this will throw
202+
if (animationScreen instanceof Context) {
203+
Object toast = makeText.invoke(null, animationScreen, "Custom filter slot installed", Toast.LENGTH_SHORT);
204+
Method show = toastClass.getMethod("show");
205+
show.invoke(toast);
206+
}
207+
} catch (Exception ignored) {
208+
}
209+
}
210+
} catch (Throwable t) {
211+
// hide toast errors; not essential
212+
t.printStackTrace();
213+
}
62214

63215
} catch (Throwable t) {
64-
Log.e("ReVancedCustomFilter", "Error in installTintField", t);
65216
t.printStackTrace();
66217
}
67218
}
68219
}
220+
221+
222+
223+
224+
225+
226+
//import android.content.Context;
227+
//import android.os.Handler;
228+
//import android.os.Looper;
229+
//import android.util.Log;
230+
//import android.widget.Toast;
231+
//
232+
//import java.lang.reflect.Constructor;
233+
//import java.lang.reflect.Method;
234+
//
235+
//public class TintFieldHook {
236+
// public static void installTintField(Object table) {
237+
// try {
238+
// Log.d("ReVancedCustomFilter", "installTintField called!");
239+
//
240+
// ClassLoader cl = table.getClass().getClassLoader();
241+
//
242+
// // Get FigureFiltersToolTable module + context
243+
// Class<?> figureFiltersClass = Class.forName(
244+
// "org.fortheloss.sticknodes.animationscreen.modules.tooltables.FigureFiltersToolTable",
245+
// false,
246+
// cl
247+
// );
248+
// Method getModule = figureFiltersClass.getMethod("getModule");
249+
// Object module = getModule.invoke(table);
250+
// Method getContext = module.getClass().getMethod("getContext");
251+
// Context context = (Context) getContext.invoke(module);
252+
//
253+
// // ✅ Show a Toast to confirm the hook actually ran
254+
// new Handler(Looper.getMainLooper()).post(() ->
255+
// Toast.makeText(context, "ReVanced filter patch active!", Toast.LENGTH_LONG).show()
256+
// );
257+
//
258+
// // Continue with your original code
259+
// Class<?> appClass = Class.forName("org.fortheloss.sticknodes.App", false, cl);
260+
// Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", false, cl);
261+
// Class<?> labelFieldClass = Class.forName("org.fortheloss.framework.LabelColorInputIncrementField", false, cl);
262+
//
263+
// Constructor<?> ctor = labelFieldClass.getConstructor(
264+
// Context.class,
265+
// String.class, String.class, int.class,
266+
// float.class, float.class, boolean.class
267+
// );
268+
//
269+
// Method localize = appClass.getMethod("localize", String.class);
270+
// String tintLabel = (String) localize.invoke(null, "tint");
271+
//
272+
// Object field = ctor.newInstance(context, tintLabel, "0.00", 4, 0.0f, 1.0f, true);
273+
//
274+
// Method registerWidget = figureFiltersClass.getMethod("registerWidget", Object.class, int.class);
275+
// registerWidget.invoke(table, field, 107);
276+
//
277+
// Method setHighFidelity = labelFieldClass.getMethod("setHighFidelity", boolean.class);
278+
// setHighFidelity.invoke(field, true);
279+
//
280+
// Object whiteColor = colorClass.getField("WHITE").get(null);
281+
// Method setValue = labelFieldClass.getMethod("setValue", colorClass);
282+
// setValue.invoke(field, whiteColor);
283+
//
284+
// Log.d("ReVancedCustomFilter", "installTintField completed successfully!");
285+
//
286+
// } catch (Throwable t) {
287+
// Log.e("ReVancedCustomFilter", "Error in installTintField", t);
288+
// t.printStackTrace();
289+
// }
290+
// }
291+
//}

patches/src/main/kotlin/sticknodes/customFilters.kt

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,9 @@ val AddCustomFilterSlot = bytecodePatch(
2828

2929

3030
execute {
31-
val match = figureFiltersInitFingerprint.patternMatch
32-
?: throw RuntimeException("Could not match FigureFiltersToolTable init")
31+
val match = figureFiltersInitFingerprint.patternMatch!!.startIndex;
3332

34-
// Now you can access match.method safely
35-
val targetMethod = figureFiltersInitFingerprint.method
36-
println("Matched method: ${targetMethod.name}")
37-
38-
targetMethod.addInstruction(0,
33+
figureFiltersInitFingerprint.method.addInstruction(match + 201,
3934
"""
4035
invoke-static {p0}, Lapp/revanced/extension/customfilters/TintFieldHook;->installTintField(Ljava/lang/Object;)V
4136
""".trimIndent()

0 commit comments

Comments
 (0)