|
10 | 10 | import java.lang.reflect.Method; |
11 | 11 | import java.lang.reflect.Field; |
12 | 12 |
|
13 | | -/** |
14 | | - * Late-injection helper. Called from smali injection. |
15 | | - * |
16 | | - * This will: |
17 | | - * - schedule retries until a) we get a usable context/module or b) timeout |
18 | | - * - when ready show a toast and attempt to add the tint field via reflection |
19 | | - */ |
20 | 13 | public class TintFieldHook { |
21 | 14 | private static final String TAG = "CustomFilterHook"; |
22 | | - private static final int INITIAL_DELAY_MS = 200; // first check delay |
23 | | - private static final int RETRY_INTERVAL_MS = 500; // interval between retries |
24 | | - private static final int MAX_RETRIES = 40; // ~20 seconds max |
25 | 15 |
|
26 | | - public static void installTintField(final Object toolTable) { |
| 16 | + // Simple ping to check injection |
| 17 | + public static void ping(Object toolTable) { |
27 | 18 | try { |
28 | | - Log.i(TAG, "installTintField() invoked, scheduling readiness checks..."); |
29 | | - scheduleAttempt(toolTable, 0); |
| 19 | + Log.i(TAG, "ping() called. toolTable class: " + (toolTable != null ? toolTable.getClass().getName() : "null")); |
30 | 20 | } catch (Throwable t) { |
31 | | - // Must never let this bubble out into the app process |
32 | | - Log.e(TAG, "installTintField top-level failure", t); |
| 21 | + Log.e(TAG, "ping() error", t); |
33 | 22 | } |
34 | 23 | } |
35 | 24 |
|
36 | | - private static void scheduleAttempt(final Object toolTable, final int attempt) { |
| 25 | + // schedule the real install a little later on the main thread |
| 26 | + public static void scheduleInstall(final Object toolTable) { |
37 | 27 | try { |
38 | | - // use main looper so any UI calls are safe |
39 | | - Handler mainHandler = new Handler(Looper.getMainLooper()); |
40 | | - int delay = (attempt == 0) ? INITIAL_DELAY_MS : RETRY_INTERVAL_MS; |
41 | | - |
42 | | - mainHandler.postDelayed(() -> { |
43 | | - try { |
44 | | - if (toolTable == null) { |
45 | | - Log.w(TAG, "toolTable is null, aborting attempts."); |
46 | | - return; |
47 | | - } |
48 | | - |
49 | | - // Try to obtain a context (via getContext() on the Module or toolTable) |
50 | | - Context context = tryGetContext(toolTable); |
51 | | - boolean hasContext = context != null; |
52 | | - |
53 | | - Log.i(TAG, "attempt " + attempt + " - context available: " + hasContext); |
54 | | - |
55 | | - // Try to get module or other indicators that initialization ran |
56 | | - Object module = tryGetModule(toolTable); |
57 | | - boolean hasModule = module != null; |
58 | | - Log.i(TAG, "attempt " + attempt + " - module available: " + hasModule + |
59 | | - (module != null ? (" (" + module.getClass().getName() + ")") : "")); |
60 | | - |
61 | | - // If we have a context -> show a toast so you can see it's working |
62 | | - if (context != null) { |
63 | | - final Context ctx = context; |
64 | | - // show toast on main thread |
65 | | - Toast.makeText(ctx, "✅ TintFieldHook triggered (late)!", Toast.LENGTH_SHORT).show(); |
66 | | - } |
67 | | - |
68 | | - // If we have module and context, attempt to install the field (safe reflect) |
69 | | - if (hasModule && hasContext) { |
70 | | - boolean installed = tryInstallField(toolTable, module, context); |
71 | | - if (installed) { |
72 | | - Log.i(TAG, "Tint field installed successfully; stopping retries."); |
73 | | - return; // success -> stop retrying |
74 | | - } else { |
75 | | - Log.i(TAG, "tryInstallField returned false (requirements not met yet)."); |
76 | | - } |
77 | | - } |
78 | | - |
79 | | - // Not ready yet -> retry until MAX_RETRIES |
80 | | - if (attempt < MAX_RETRIES) { |
81 | | - scheduleAttempt(toolTable, attempt + 1); |
82 | | - } else { |
83 | | - Log.w(TAG, "TintFieldHook: reached max retries, giving up."); |
| 28 | + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { |
| 29 | + @Override |
| 30 | + public void run() { |
| 31 | + try { |
| 32 | + installTintField(toolTable); |
| 33 | + } catch (Throwable t) { |
| 34 | + Log.e(TAG, "scheduled install failed", t); |
84 | 35 | } |
85 | | - |
86 | | - } catch (Throwable inner) { |
87 | | - // swallow and continue retrying to avoid crashing the host app |
88 | | - Log.e(TAG, "Error during scheduled attempt", inner); |
89 | | - if (attempt < MAX_RETRIES) scheduleAttempt(toolTable, attempt + 1); |
90 | | - } |
91 | | - }, delay); |
92 | | - } catch (Throwable t) { |
93 | | - Log.e(TAG, "Failed scheduling attempt", t); |
94 | | - } |
95 | | - } |
96 | | - |
97 | | - /** |
98 | | - * Try to call toolTable.getModule() or fallback to other getters. |
99 | | - */ |
100 | | - private static Object tryGetModule(Object toolTable) { |
101 | | - try { |
102 | | - // Many ToolTable classes have getModule() (in your earlier decompiled code it did) |
103 | | - Method getModule = null; |
104 | | - try { |
105 | | - getModule = toolTable.getClass().getMethod("getModule"); |
106 | | - } catch (NoSuchMethodException ignored) { |
107 | | - } |
108 | | - if (getModule != null) { |
109 | | - Object module = getModule.invoke(toolTable); |
110 | | - if (module != null) return module; |
111 | | - } |
112 | | - |
113 | | - // Some implementations may store a 'module' field; check reflectively |
114 | | - try { |
115 | | - Field moduleField = toolTable.getClass().getDeclaredField("mModule"); |
116 | | - moduleField.setAccessible(true); |
117 | | - Object module = moduleField.get(toolTable); |
118 | | - if (module != null) return module; |
119 | | - } catch (NoSuchFieldException ignored) { |
120 | | - } |
121 | | - |
122 | | - } catch (Throwable t) { |
123 | | - Log.w(TAG, "tryGetModule failed", t); |
124 | | - } |
125 | | - return null; |
126 | | - } |
127 | | - |
128 | | - /** |
129 | | - * Try to obtain a Context. Try several strategies safely. |
130 | | - */ |
131 | | - private static Context tryGetContext(Object toolTable) { |
132 | | - try { |
133 | | - // 1) direct getContext() on toolTable |
134 | | - try { |
135 | | - Method getContext = toolTable.getClass().getMethod("getContext"); |
136 | | - Object ctx = getContext.invoke(toolTable); |
137 | | - if (ctx instanceof Context) return (Context) ctx; |
138 | | - } catch (NoSuchMethodException ignored) { |
139 | | - } |
140 | | - |
141 | | - // 2) via module.getContext() |
142 | | - Object module = tryGetModule(toolTable); |
143 | | - if (module != null) { |
144 | | - try { |
145 | | - Method getContext = module.getClass().getMethod("getContext"); |
146 | | - Object ctx = getContext.invoke(module); |
147 | | - if (ctx instanceof Context) return (Context) ctx; |
148 | | - } catch (NoSuchMethodException ignored) { |
149 | 36 | } |
150 | | - } |
151 | | - |
152 | | - // 3) try a common field lookup (last resort) |
153 | | - try { |
154 | | - Field ctxField = toolTable.getClass().getDeclaredField("mContext"); |
155 | | - ctxField.setAccessible(true); |
156 | | - Object ctx = ctxField.get(toolTable); |
157 | | - if (ctx instanceof Context) return (Context) ctx; |
158 | | - } catch (NoSuchFieldException ignored) { |
159 | | - } |
160 | | - |
| 37 | + }, 200); // 200 ms delay |
161 | 38 | } catch (Throwable t) { |
162 | | - Log.w(TAG, "tryGetContext failed", t); |
| 39 | + Log.e(TAG, "scheduleInstall failed", t); |
163 | 40 | } |
164 | | - return null; |
165 | 41 | } |
166 | 42 |
|
167 | | - /** |
168 | | - * Attempt to create & register the tint field via reflection. |
169 | | - * Returns true on success (field created and registered), false if not enough requirements yet. |
170 | | - * |
171 | | - * This method purposely tolerates missing constructors/methods and will NOT throw errors up. |
172 | | - */ |
173 | | - private static boolean tryInstallField(Object toolTable, Object module, Context context) { |
| 43 | + // do minimal, defensive reflection/UI code here |
| 44 | + public static void installTintField(Object table) { |
174 | 45 | try { |
175 | | - // We want to construct a LabelColorInputIncrementField similar to the app's code: |
176 | | - // new LabelColorInputIncrementField(animationScreen, localizedLabel, "0.00", 4, 0.0f, 1.0f, true); |
177 | | - // Then call registerWidget(table, 107) and setHighFidelity(true), setValue(Color.WHITE) etc. |
178 | | - |
179 | | - ClassLoader cl = toolTable.getClass().getClassLoader(); |
180 | | - |
181 | | - // Load classes reflectively; if any missing, bail out gracefully |
182 | | - Class<?> labelColorFieldClass; |
183 | | - try { |
184 | | - labelColorFieldClass = Class.forName("org.fortheloss.framework.LabelColorInputIncrementField", false, cl); |
185 | | - } catch (Throwable e) { |
186 | | - Log.i(TAG, "LabelColorInputIncrementField class not present yet."); |
187 | | - return false; // not ready to create field |
188 | | - } |
189 | | - |
190 | | - // Need animation screen (module.getContext()) |
191 | | - Object animationScreen = null; |
192 | | - try { |
193 | | - Method getContext = module.getClass().getMethod("getContext"); |
194 | | - animationScreen = getContext.invoke(module); |
195 | | - } catch (NoSuchMethodException ignored) { |
196 | | - } |
197 | | - if (animationScreen == null) { |
198 | | - Log.i(TAG, "animationScreen not available yet."); |
199 | | - return false; |
200 | | - } |
201 | | - |
202 | | - // Localization: App.localize("tint") |
203 | | - String tintLabel = "tint"; |
| 46 | + // defensive: attempt to find context safely |
| 47 | + Object contextObj = null; |
204 | 48 | try { |
205 | | - Class<?> appClass = Class.forName("org.fortheloss.sticknodes.App", false, cl); |
206 | | - Method localize = appClass.getMethod("localize", String.class); |
207 | | - Object res = localize.invoke(null, "tint"); |
208 | | - if (res instanceof String) tintLabel = (String) res; |
209 | | - } catch (Throwable t) { |
210 | | - Log.w(TAG, "Could not localize 'tint', using fallback", t); |
| 49 | + // In a lot of these classes getModule().getContext() is the path; adapt as needed |
| 50 | + java.lang.reflect.Method getModule = table.getClass().getMethod("getModule"); |
| 51 | + Object module = getModule.invoke(table); |
| 52 | + java.lang.reflect.Method getContext = module.getClass().getMethod("getContext"); |
| 53 | + contextObj = getContext.invoke(module); |
| 54 | + } catch (Throwable inner) { |
| 55 | + Log.w(TAG, "couldn't get context via getModule/getContext", inner); |
211 | 56 | } |
212 | 57 |
|
213 | | - // Find the constructor signature: |
214 | | - // <init>(AnimationScreen, String, String, int, float, float, boolean) |
215 | | - Constructor<?> ctor = null; |
216 | | - for (Constructor<?> c : labelColorFieldClass.getDeclaredConstructors()) { |
217 | | - Class<?>[] params = c.getParameterTypes(); |
218 | | - if (params.length >= 7) { // loose matching (some builds may differ) |
219 | | - ctor = c; |
220 | | - break; |
221 | | - } |
222 | | - } |
223 | | - if (ctor == null) { |
224 | | - Log.i(TAG, "No suitable constructor found for LabelColorInputIncrementField yet."); |
225 | | - return false; |
226 | | - } |
227 | | - |
228 | | - ctor.setAccessible(true); |
229 | | - |
230 | | - // Create instance (safely) |
231 | | - Object fieldInstance; |
232 | | - try { |
233 | | - // fallback param values (match the game's constructor as best as possible) |
234 | | - Object[] args = new Object[] { animationScreen, tintLabel, "0.00", Integer.valueOf(4), |
235 | | - Float.valueOf(0.0f), Float.valueOf(1.0f), Boolean.TRUE }; |
236 | | - fieldInstance = ctor.newInstance(args); |
237 | | - } catch (Throwable t) { |
238 | | - Log.w(TAG, "Constructor invocation failed", t); |
239 | | - return false; |
240 | | - } |
241 | | - |
242 | | - // registerWidget(table, id) |
243 | | - try { |
244 | | - Method registerWidget = toolTable.getClass().getMethod("registerWidget", Object.class, int.class); |
245 | | - registerWidget.invoke(toolTable, fieldInstance, Integer.valueOf(107)); |
246 | | - } catch (NoSuchMethodException nm) { |
247 | | - // try find in superclass |
| 58 | + // fallback: try table.getContext() |
| 59 | + if (contextObj == null) { |
248 | 60 | try { |
249 | | - Method registerWidget = toolTable.getClass().getMethod("registerWidget", Object.class, Integer.TYPE); |
250 | | - registerWidget.invoke(toolTable, fieldInstance, Integer.valueOf(107)); |
251 | | - } catch (Throwable t) { |
252 | | - Log.w(TAG, "registerWidget not available; cannot register field", t); |
253 | | - // still proceed to add to UI if possible (but bail as not fully integrated) |
254 | | - return false; |
255 | | - } |
256 | | - } catch (Throwable t) { |
257 | | - Log.w(TAG, "Failed to invoke registerWidget", t); |
258 | | - return false; |
| 61 | + java.lang.reflect.Method getContext2 = table.getClass().getMethod("getContext"); |
| 62 | + contextObj = getContext2.invoke(table); |
| 63 | + } catch (Throwable ignored) {} |
259 | 64 | } |
260 | 65 |
|
261 | | - // setHighFidelity(true) |
262 | | - try { |
263 | | - Method setHighFidelity = fieldInstance.getClass().getMethod("setHighFidelity", boolean.class); |
264 | | - setHighFidelity.invoke(fieldInstance, Boolean.TRUE); |
265 | | - } catch (Throwable ignored) { |
266 | | - Log.i(TAG, "setHighFidelity not present or failed - ignoring"); |
267 | | - } |
| 66 | + Context context = (contextObj instanceof Context) ? (Context) contextObj : null; |
268 | 67 |
|
269 | | - // set default color value if possible: setValue(Color.WHITE) |
270 | | - try { |
271 | | - Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", false, cl); |
272 | | - Field white = colorClass.getField("WHITE"); |
273 | | - Object whiteColor = white.get(null); |
274 | | - Method setValue = fieldInstance.getClass().getMethod("setValue", colorClass); |
275 | | - setValue.invoke(fieldInstance, whiteColor); |
276 | | - } catch (Throwable ignored) { |
277 | | - Log.i(TAG, "Could not set default color - ignoring"); |
| 68 | + // Show a toast safely on main thread if we have a context |
| 69 | + if (context != null) { |
| 70 | + final Context ctx = context; |
| 71 | + new Handler(Looper.getMainLooper()).post(() -> |
| 72 | + Toast.makeText(ctx, "✅ Custom filter slot installed", Toast.LENGTH_SHORT).show() |
| 73 | + ); |
278 | 74 | } |
279 | 75 |
|
280 | | - Log.i(TAG, "tryInstallField: created and registered field instance: " + fieldInstance.getClass().getName()); |
281 | | - // Success |
282 | | - return true; |
283 | | - |
| 76 | + Log.i(TAG, "installTintField() finished; table class: " + (table != null ? table.getClass().getName() : "null")); |
284 | 77 | } catch (Throwable t) { |
285 | | - Log.e(TAG, "Unexpected error in tryInstallField", t); |
286 | | - return false; |
| 78 | + Log.e(TAG, "installTintField failed", t); |
287 | 79 | } |
288 | 80 | } |
289 | 81 | } |
290 | | - |
291 | | -/** |
292 | | - * Reflection-only helper that creates a "Custom Tint" slot in FigureFiltersToolTable. |
293 | | - * |
294 | | - * Call: CustomTintSlotHook.installCustomTintSlot(tableObject); |
295 | | - */ |
296 | | - |
297 | 82 | //public class TintFieldHook { |
298 | 83 | // public static void installCustomTintSlot(Object table) { |
299 | 84 | // try { |
|
0 commit comments