Overlay.java
| 1 | package com.drmod; |
| 2 | |
| 3 | import android.content.Context; |
| 4 | import android.graphics.Color; |
| 5 | import android.graphics.PixelFormat; |
| 6 | import android.os.Build; |
| 7 | import android.os.Handler; |
| 8 | import android.os.Looper; |
| 9 | import android.provider.Settings; |
| 10 | import android.view.ContextThemeWrapper; |
| 11 | import android.view.Gravity; |
| 12 | import android.view.MotionEvent; |
| 13 | import android.view.View; |
| 14 | import android.view.ViewGroup; |
| 15 | import android.view.WindowManager; |
| 16 | import android.widget.Button; |
| 17 | import android.widget.CompoundButton; |
| 18 | import android.widget.FrameLayout; |
| 19 | import android.widget.LinearLayout; |
| 20 | import android.widget.TextView; |
| 21 | import android.widget.ToggleButton; |
| 22 | |
| 23 | public class Overlay { |
| 24 | private static final String TAG = "DRMod-Overlay"; |
| 25 | private static native void nativeToggle(int feature, boolean enabled); |
| 26 | |
| 27 | // Stato condiviso: [0]=menu aperto, [1]=overlay nascosto (hitbox solo tocco) |
| 28 | public static void init(final Context ctx) { |
| 29 | DLog.log(TAG, "init() entered, ctx=" + (ctx != null ? ctx.getClass().getName() : "null")); |
| 30 | try { |
| 31 | if (Build.VERSION.SDK_INT >= 23 && !Settings.canDrawOverlays(ctx)) { |
| 32 | DLog.log(TAG, "canDrawOverlays=false, attempting anyway"); |
| 33 | } else { |
| 34 | DLog.log(TAG, "canDrawOverlays=true"); |
| 35 | } |
| 36 | } catch (Throwable t) { |
| 37 | DLog.log(TAG, "Permission check failed: " + t.getMessage()); |
| 38 | } |
| 39 | |
| 40 | final Context themedCtx = new ContextThemeWrapper(ctx, android.R.style.Theme_DeviceDefault); |
| 41 | final WindowManager wm = (WindowManager) themedCtx.getSystemService(Context.WINDOW_SERVICE); |
| 42 | DLog.log(TAG, "themedCtx created, wm=" + (wm != null ? "ok" : "NULL")); |
| 43 | |
| 44 | // ===== Menu (contenuto, liberamente trascinabile) ===== |
| 45 | final LinearLayout menu = new LinearLayout(themedCtx); |
| 46 | menu.setOrientation(LinearLayout.VERTICAL); |
| 47 | menu.setBackgroundColor(0xCC111111); |
| 48 | menu.setPadding(dp(themedCtx, 12), dp(themedCtx, 12), dp(themedCtx, 12), dp(themedCtx, 12)); |
| 49 | |
| 50 | TextView title = new TextView(themedCtx); |
| 51 | title.setText("DungeonRush Mod"); |
| 52 | title.setTextColor(Color.WHITE); |
| 53 | title.setTextSize(14); |
| 54 | title.setPadding(0, 0, 0, dp(themedCtx, 2)); |
| 55 | menu.addView(title); |
| 56 | |
| 57 | TextView sub = new TextView(themedCtx); |
| 58 | sub.setText(" by TeamModdersITA"); |
| 59 | sub.setTextColor(0xFF999999); |
| 60 | sub.setTextSize(10); |
| 61 | sub.setPadding(0, 0, 0, dp(themedCtx, 8)); |
| 62 | menu.addView(sub); |
| 63 | |
| 64 | String[] names = {"Immortality", "Forced Crits", "Divine Forge", "Divine Overdrive", "Minigame Loot", "Forge Exp Substats"}; |
| 65 | for (int i = 0; i < names.length; i++) { |
| 66 | final int idx = i; |
| 67 | ToggleButton btn = new ToggleButton(themedCtx); |
| 68 | btn.setTextOn(names[i] + " [ON]"); |
| 69 | btn.setTextOff(names[i] + " [OFF]"); |
| 70 | btn.setChecked(false); |
| 71 | btn.setTextSize(11); |
| 72 | btn.setBackgroundColor(0xFF222222); |
| 73 | btn.setTextColor(Color.WHITE); |
| 74 | btn.setPadding(dp(themedCtx, 8), dp(themedCtx, 4), dp(themedCtx, 8), dp(themedCtx, 4)); |
| 75 | LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( |
| 76 | LinearLayout.LayoutParams.MATCH_PARENT, |
| 77 | LinearLayout.LayoutParams.WRAP_CONTENT); |
| 78 | lp.bottomMargin = dp(themedCtx, 4); |
| 79 | btn.setLayoutParams(lp); |
| 80 | btn.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { |
| 81 | public void onCheckedChanged(CompoundButton v, boolean checked) { |
| 82 | nativeToggle(idx, checked); |
| 83 | } |
| 84 | }); |
| 85 | menu.addView(btn); |
| 86 | } |
| 87 | |
| 88 | TextView sep = new TextView(themedCtx); |
| 89 | sep.setText("\nAlways ON:"); |
| 90 | sep.setTextColor(0xFF888888); |
| 91 | sep.setTextSize(10); |
| 92 | menu.addView(sep); |
| 93 | |
| 94 | TextView status = new TextView(themedCtx); |
| 95 | status.setText(" Ad Skip\n Store Bypass\n ACTk Disable"); |
| 96 | status.setTextColor(0xFF666666); |
| 97 | status.setTextSize(10); |
| 98 | menu.addView(status); |
| 99 | |
| 100 | DLog.log(TAG, "menu built with " + names.length + " toggles"); |
| 101 | |
| 102 | // ===== FAB (ancora fissa, separata dal menu) ===== |
| 103 | final Button fab = new Button(themedCtx); |
| 104 | fab.setText("DR"); |
| 105 | fab.setTextSize(10); |
| 106 | fab.setBackgroundColor(0xAAFF3333); |
| 107 | fab.setTextColor(Color.WHITE); |
| 108 | fab.setPadding(0, 0, 0, 0); |
| 109 | fab.setMinWidth(dp(themedCtx, 40)); |
| 110 | fab.setMinimumWidth(dp(themedCtx, 40)); |
| 111 | fab.setMinHeight(dp(themedCtx, 40)); |
| 112 | fab.setMinimumHeight(dp(themedCtx, 40)); |
| 113 | |
| 114 | // Wrapper separati, ognuno è una view indipendente agganciata al decor. |
| 115 | final FrameLayout menuRoot = new FrameLayout(themedCtx); |
| 116 | menuRoot.addView(menu, new FrameLayout.LayoutParams( |
| 117 | FrameLayout.LayoutParams.WRAP_CONTENT, |
| 118 | FrameLayout.LayoutParams.WRAP_CONTENT)); |
| 119 | |
| 120 | final FrameLayout fabRoot = new FrameLayout(themedCtx); |
| 121 | fabRoot.addView(fab, new FrameLayout.LayoutParams( |
| 122 | dp(themedCtx, 44), dp(themedCtx, 44))); |
| 123 | DLog.log(TAG, "menuRoot + fabRoot built (separate)"); |
| 124 | |
| 125 | // Stato: [0]=menu aperto, [1]=nascosto (fab alpha 0 ma toccabile) |
| 126 | final boolean[] state = {false, false}; |
| 127 | |
| 128 | // Apertura/chiusura menu con doppio click sul FAB? No: click singolo. |
| 129 | fab.setOnClickListener(new View.OnClickListener() { |
| 130 | public void onClick(View v) { |
| 131 | state[1] = false; // riprende da hide comunque |
| 132 | state[0] = !state[0]; |
| 133 | if (state[0]) { |
| 134 | // Rendi visibile il menu e posizionalo accanto al FAB (dimensioni fisse). |
| 135 | menuRoot.setVisibility(View.VISIBLE); |
| 136 | placeMenuNearFab(menuRoot, fabRoot, themedCtx); |
| 137 | fabRoot.setVisibility(View.GONE); // overlay sparisce quando menu aperto |
| 138 | } else { |
| 139 | menuRoot.setVisibility(View.GONE); |
| 140 | fabRoot.setVisibility(View.VISIBLE); |
| 141 | fab.setAlpha(1f); |
| 142 | } |
| 143 | } |
| 144 | }); |
| 145 | |
| 146 | // Drag separati: il menu sposta solo menuRoot, il fab sposta solo fabRoot. |
| 147 | makeDraggable(menu, menuRoot, state); |
| 148 | makeDraggable(fab, fabRoot, state); |
| 149 | DLog.log(TAG, "draggable set (menu->menuRoot, fab->fabRoot)"); |
| 150 | |
| 151 | // Bottoni hide/close (vivono nel menu e agiscono su menuRoot + fab) |
| 152 | buildButtons(menu, menuRoot, fabRoot, fab, state, themedCtx); |
| 153 | |
| 154 | // All'avvio il menu è nascosto; il FAB resta l'ancora visibile per aprirlo. |
| 155 | menuRoot.setVisibility(View.GONE); |
| 156 | |
| 157 | DLog.log(TAG, "scheduling addView on main looper..."); |
| 158 | new Handler(Looper.getMainLooper()).post(new Runnable() { |
| 159 | |
| 160 | public void run() { |
| 161 | DLog.log(TAG, "addView runnable started on thread=" |
| 162 | + Thread.currentThread().getName()); |
| 163 | boolean attached = false; |
| 164 | try { |
| 165 | attached = attachToActivityDecor(menuRoot, fabRoot, themedCtx); |
| 166 | DLog.log(TAG, "attachToActivityDecor=" + attached); |
| 167 | } catch (Throwable t) { |
| 168 | DLog.log(TAG, "attachToActivityDecor exception: " + t); |
| 169 | } |
| 170 | if (!attached) { |
| 171 | try { |
| 172 | WindowManager.LayoutParams mParams = makeWmParams(themedCtx); |
| 173 | WindowManager.LayoutParams fParams = makeWmParams(themedCtx); |
| 174 | fParams.y = dp(themedCtx, 100); |
| 175 | wm.addView(menuRoot, mParams); |
| 176 | wm.addView(fabRoot, fParams); |
| 177 | DLog.log(TAG, "wm.addView fallback succeeded (menu+fab)"); |
| 178 | } catch (Throwable t) { |
| 179 | DLog.log(TAG, "wm.addView fallback failed: " + t); |
| 180 | } |
| 181 | } |
| 182 | } |
| 183 | }); |
| 184 | DLog.log(TAG, "init() complete"); |
| 185 | } |
| 186 | |
| 187 | // ===== Punto di aggancio al decor ===== |
| 188 | private static boolean attachToActivityDecor(View menuChild, View fabChild, Context ctx) { |
| 189 | try { |
| 190 | Class<?> atCls = Class.forName("android.app.ActivityThread"); |
| 191 | Object at = atCls.getMethod("currentActivityThread").invoke(null); |
| 192 | if (at == null) { |
| 193 | DLog.log(TAG, "attach: ActivityThread null"); |
| 194 | return false; |
| 195 | } |
| 196 | java.lang.reflect.Field mActivities = |
| 197 | atCls.getDeclaredField("mActivities"); |
| 198 | mActivities.setAccessible(true); |
| 199 | Object activities = mActivities.get(at); |
| 200 | DLog.log(TAG, "attach: mActivities=" + activities); |
| 201 | if (activities == null) return false; |
| 202 | |
| 203 | Object record = null; |
| 204 | if (activities instanceof java.util.Map) { |
| 205 | java.util.Collection<?> vals = |
| 206 | ((java.util.Map<?, ?>) activities).values(); |
| 207 | if (!vals.isEmpty()) { |
| 208 | record = vals.iterator().next(); |
| 209 | } |
| 210 | } |
| 211 | DLog.log(TAG, "attach: record=" + record); |
| 212 | if (record == null) return false; |
| 213 | |
| 214 | java.lang.reflect.Field actField = |
| 215 | record.getClass().getDeclaredField("activity"); |
| 216 | actField.setAccessible(true); |
| 217 | Object activity = actField.get(record); |
| 218 | DLog.log(TAG, "attach: activity=" + activity); |
| 219 | if (!(activity instanceof android.app.Activity)) return false; |
| 220 | |
| 221 | android.view.ViewGroup decor = |
| 222 | (android.view.ViewGroup) ((android.app.Activity) activity) |
| 223 | .getWindow().getDecorView(); |
| 224 | DLog.log(TAG, "attach: decor=" + decor); |
| 225 | |
| 226 | // Menu: default in alto a sinistra, indipendente. |
| 227 | android.widget.FrameLayout.LayoutParams mLp = |
| 228 | new android.widget.FrameLayout.LayoutParams( |
| 229 | android.widget.FrameLayout.LayoutParams.WRAP_CONTENT, |
| 230 | android.widget.FrameLayout.LayoutParams.WRAP_CONTENT, |
| 231 | android.view.Gravity.TOP | android.view.Gravity.START); |
| 232 | mLp.topMargin = dp(ctx, 120); |
| 233 | mLp.leftMargin = dp(ctx, 60); |
| 234 | |
| 235 | // FAB: ancorato in alto a sinistra (dove l'ho aperto), indipendente. |
| 236 | android.widget.FrameLayout.LayoutParams fLp = |
| 237 | new android.widget.FrameLayout.LayoutParams( |
| 238 | android.widget.FrameLayout.LayoutParams.WRAP_CONTENT, |
| 239 | android.widget.FrameLayout.LayoutParams.WRAP_CONTENT, |
| 240 | android.view.Gravity.TOP | android.view.Gravity.START); |
| 241 | fLp.topMargin = dp(ctx, 100); |
| 242 | fLp.leftMargin = dp(ctx, 8); |
| 243 | |
| 244 | decor.addView(menuChild, mLp); |
| 245 | decor.addView(fabChild, fLp); |
| 246 | DLog.log(TAG, "attach: menu+fab attached to decor"); |
| 247 | return true; |
| 248 | } catch (Throwable t) { |
| 249 | DLog.log(TAG, "attachToActivityDecor exception: " + t); |
| 250 | return false; |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | private static WindowManager.LayoutParams makeWmParams(Context ctx) { |
| 255 | int overlayType = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; |
| 256 | if (Build.VERSION.SDK_INT < 26) { |
| 257 | overlayType = WindowManager.LayoutParams.TYPE_PHONE; |
| 258 | } |
| 259 | WindowManager.LayoutParams p = new WindowManager.LayoutParams( |
| 260 | WindowManager.LayoutParams.WRAP_CONTENT, |
| 261 | WindowManager.LayoutParams.WRAP_CONTENT, |
| 262 | overlayType, |
| 263 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
| 264 | | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, |
| 265 | PixelFormat.TRANSLUCENT); |
| 266 | p.gravity = Gravity.TOP | Gravity.START; |
| 267 | p.x = dp(ctx, 8); |
| 268 | p.y = dp(ctx, 100); |
| 269 | return p; |
| 270 | } |
| 271 | |
| 272 | // ===== Hide / Close ===== |
| 273 | private static void buildButtons(final LinearLayout menu, final FrameLayout menuRoot, |
| 274 | final FrameLayout fabRoot, final Button fab, final boolean[] state, Context ctx) { |
| 275 | try { |
| 276 | LinearLayout btnRow = new LinearLayout(ctx); |
| 277 | btnRow.setOrientation(LinearLayout.HORIZONTAL); |
| 278 | LinearLayout.LayoutParams rowLp = new LinearLayout.LayoutParams( |
| 279 | LinearLayout.LayoutParams.MATCH_PARENT, |
| 280 | LinearLayout.LayoutParams.WRAP_CONTENT); |
| 281 | rowLp.topMargin = dp(ctx, 4); |
| 282 | btnRow.setLayoutParams(rowLp); |
| 283 | |
| 284 | Button hideBtn = new Button(ctx); |
| 285 | hideBtn.setText("hide"); |
| 286 | hideBtn.setTextSize(11); |
| 287 | hideBtn.setBackgroundColor(0xFF222222); |
| 288 | hideBtn.setTextColor(Color.WHITE); |
| 289 | |
| 290 | Button closeBtn = new Button(ctx); |
| 291 | closeBtn.setText("close"); |
| 292 | closeBtn.setTextSize(11); |
| 293 | closeBtn.setBackgroundColor(0xFF222222); |
| 294 | closeBtn.setTextColor(Color.WHITE); |
| 295 | |
| 296 | btnRow.addView(hideBtn, new LinearLayout.LayoutParams( |
| 297 | 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); |
| 298 | btnRow.addView(closeBtn, new LinearLayout.LayoutParams( |
| 299 | 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); |
| 300 | menu.addView(btnRow); |
| 301 | |
| 302 | hideBtn.setOnClickListener(new View.OnClickListener() { |
| 303 | public void onClick(View v) { |
| 304 | state[0] = false; |
| 305 | state[1] = true; |
| 306 | menuRoot.setVisibility(View.GONE); |
| 307 | fabRoot.setVisibility(View.VISIBLE); |
| 308 | fab.setAlpha(0f); // ancora nascosta ma toccabile per riaprire |
| 309 | } |
| 310 | }); |
| 311 | |
| 312 | closeBtn.setOnClickListener(new View.OnClickListener() { |
| 313 | public void onClick(View v) { |
| 314 | state[0] = false; |
| 315 | state[1] = false; |
| 316 | menuRoot.setVisibility(View.GONE); |
| 317 | fabRoot.setVisibility(View.VISIBLE); |
| 318 | fab.setAlpha(1f); |
| 319 | } |
| 320 | }); |
| 321 | DLog.log(TAG, "hide/close listeners attached"); |
| 322 | } catch (Throwable t) { |
| 323 | DLog.log(TAG, "buildButtons EXCEPTION: " + t); |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | // ===== Drag: sposta SOLO il view indicato (moveView), clamp + size fissa ===== |
| 328 | private static void makeDraggable(final View touchView, final View moveView, |
| 329 | final boolean[] state) { |
| 330 | touchView.setOnTouchListener(new View.OnTouchListener() { |
| 331 | int lastX, lastY; |
| 332 | boolean moved; |
| 333 | public boolean onTouch(View v, MotionEvent event) { |
| 334 | if (state[1]) return false; |
| 335 | switch (event.getActionMasked()) { |
| 336 | case MotionEvent.ACTION_DOWN: |
| 337 | lastX = (int) event.getRawX(); |
| 338 | lastY = (int) event.getRawY(); |
| 339 | moved = false; |
| 340 | return true; |
| 341 | case MotionEvent.ACTION_MOVE: |
| 342 | int dx = (int) event.getRawX() - lastX; |
| 343 | int dy = (int) event.getRawY() - lastY; |
| 344 | if (Math.abs(dx) > 5 || Math.abs(dy) > 5) moved = true; |
| 345 | lastX = (int) event.getRawX(); |
| 346 | lastY = (int) event.getRawY(); |
| 347 | if (moveView.getParent() != null) { |
| 348 | moveClamped(moveView, dx, dy); |
| 349 | } |
| 350 | return true; |
| 351 | case MotionEvent.ACTION_UP: |
| 352 | if (!moved) { |
| 353 | v.performClick(); |
| 354 | } |
| 355 | return moved; |
| 356 | } |
| 357 | return false; |
| 358 | } |
| 359 | }); |
| 360 | DLog.log(TAG, "draggable attached: touch=" + touchView.getClass().getSimpleName() |
| 361 | + " move=" + moveView.getClass().getSimpleName()); |
| 362 | } |
| 363 | |
| 364 | // Sposta il view con: size fissa (no rimpicciolimento AT_MOST) + clamp a schermo. |
| 365 | private static void moveClamped(View view, int dx, int dy) { |
| 366 | ViewGroup.LayoutParams lp = view.getLayoutParams(); |
| 367 | if (!(lp instanceof ViewGroup.MarginLayoutParams)) { |
| 368 | view.setTranslationX(view.getTranslationX() + dx); |
| 369 | view.setTranslationY(view.getTranslationY() + dy); |
| 370 | return; |
| 371 | } |
| 372 | ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp; |
| 373 | int w = view.getWidth(); |
| 374 | int h = view.getHeight(); |
| 375 | if (w <= 0) w = view.getMeasuredWidth(); |
| 376 | if (h <= 0) h = view.getMeasuredHeight(); |
| 377 | boolean known = (w > 0 && h > 0); |
| 378 | if (known) { |
| 379 | mlp.width = w; |
| 380 | mlp.height = h; |
| 381 | } |
| 382 | int left = mlp.leftMargin + dx; |
| 383 | int top = mlp.topMargin + dy; |
| 384 | View parent = (View) view.getParent(); |
| 385 | if (parent != null) { |
| 386 | int parentW = parent.getWidth(); |
| 387 | int parentH = parent.getHeight(); |
| 388 | left = Math.max(0, left); |
| 389 | top = Math.max(0, top); |
| 390 | if (known && parentW > 0) left = Math.min(left, Math.max(0, parentW - w)); |
| 391 | if (known && parentH > 0) top = Math.min(top, Math.max(0, parentH - h)); |
| 392 | } else { |
| 393 | if (left < 0) left = 0; |
| 394 | if (top < 0) top = 0; |
| 395 | } |
| 396 | mlp.leftMargin = left; |
| 397 | mlp.topMargin = top; |
| 398 | view.setLayoutParams(mlp); |
| 399 | } |
| 400 | |
| 401 | // Posiziona il menu accanto al FAB (a destra se c'è spazio, altrimenti a sinistra), |
| 402 | // sempre con clamp a schermo. |
| 403 | private static void placeMenuNearFab(View menuRoot, View fabRoot, Context ctx) { |
| 404 | ViewGroup.LayoutParams mlp0 = menuRoot.getLayoutParams(); |
| 405 | ViewGroup.LayoutParams flp0 = fabRoot.getLayoutParams(); |
| 406 | int fx = 8, fy = 100; |
| 407 | if (flp0 instanceof ViewGroup.MarginLayoutParams) { |
| 408 | ViewGroup.MarginLayoutParams f = (ViewGroup.MarginLayoutParams) flp0; |
| 409 | fx = f.leftMargin; |
| 410 | fy = f.topMargin; |
| 411 | } |
| 412 | int fw = fabRoot.getWidth(); |
| 413 | if (fw <= 0) fw = dp(ctx, 44); |
| 414 | |
| 415 | int screenW = ctx.getResources().getDisplayMetrics().widthPixels; |
| 416 | int screenH = ctx.getResources().getDisplayMetrics().heightPixels; |
| 417 | |
| 418 | // Dimensioni del menu hardcodate (calibrate a schermo 720x1448 dalla misurazione reale). |
| 419 | // Non serve misurare né aprire invisibile: usiamo direttamente i valori fissi. |
| 420 | int mw = 350; |
| 421 | int mh = 771; |
| 422 | |
| 423 | int left = fx + fw; |
| 424 | int top = fy; |
| 425 | if (left + mw > screenW) left = fx - mw; // a sinistra del fab se non c'è spazio |
| 426 | left = Math.max(0, left); |
| 427 | top = Math.max(0, top); |
| 428 | if (left + mw > screenW) left = Math.max(0, screenW - mw); |
| 429 | if (top + mh > screenH) top = Math.max(0, screenH - mh); |
| 430 | |
| 431 | if (mlp0 instanceof ViewGroup.MarginLayoutParams) { |
| 432 | ViewGroup.MarginLayoutParams m = (ViewGroup.MarginLayoutParams) mlp0; |
| 433 | m.leftMargin = left; |
| 434 | m.topMargin = top; |
| 435 | menuRoot.setLayoutParams(m); |
| 436 | } |
| 437 | DLog.log(TAG, "placeMenuNearFab -> left=" + left + " top=" + top + " mw=" + mw + " mh=" + mh); |
| 438 | } |
| 439 | |
| 440 | private static int dp(Context ctx, int val) { |
| 441 | return (int) (val * ctx.getResources().getDisplayMetrics().density); |
| 442 | } |
| 443 | } |