From 9c3fa89b660ed1252d7ed90df0186156d9f6febe Mon Sep 17 00:00:00 2001 From: Dhangofa <117024745+Dhangofa@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:28:58 +0530 Subject: [PATCH 01/10] Improve execution mode selection and permission handling in MainActivity - Added MODE_NONE as default execution mode to avoid auto-selecting Root or Shizuku on fresh install. - Updated first-launch behavior to show setup prompt instead of assuming Root mode. - Added Root permission check using `su -c id` when Root mode is selected or restored. - Re-check Root permission on app open if Root mode was previously saved. - Changed Shizuku flow so permission is requested only when user manually selects Shizuku mode. - For saved Shizuku mode, only check current Shizuku status without triggering permission popup. - Added Shizuku binder received/dead/result listeners with current-mode validation. - Prevented Shizuku callbacks from overwriting Root mode status. - Prevented stale Root permission check result from overwriting UI if user switches mode during check. - Wrapped Shizuku permission checks in try/catch for safer binder failure handling. - Removed listener references in onDestroy to avoid memory leaks. --- .../dhangofa/networktoggle/MainActivity.java | 138 ++++++++++++++---- 1 file changed, 106 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java index 62f1a79..8977805 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java +++ b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java @@ -1,3 +1,13 @@ +/* +Behavior: +- Fresh install: no option selected. +- Root selected: root permission checked. +- Shizuku selected: Shizuku permission checked/requested only then. +- Saved Root: root rechecked on open. +- Saved Shizuku: status checked without auto-popup. +- Shizuku callbacks update UI only when Shizuku mode is selected. +*/ + package com.dhangofa.networktoggle; import android.app.Activity; @@ -7,12 +17,17 @@ import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.TextView; + import rikka.shizuku.Shizuku; public class MainActivity extends Activity { private static final String PREFS_NAME = "NetTogglePrefs"; private static final String EXEC_MODE_KEY = "exec_mode"; + + private static final int MODE_NONE = 0; + private static final int MODE_ROOT = 1; + private static final int MODE_SHIZUKU = 2; private RadioGroup radioGroup; private RadioButton radioRoot; @@ -20,22 +35,33 @@ public class MainActivity extends Activity { private TextView statusText; private SharedPreferences prefs; - // 1. Wait for Shizuku Binder to be injected, then check permissions + // 1. First check Shizuku mode selected then wait for Shizuku Binder to be injected, then check permissions private final Shizuku.OnBinderReceivedListener binderReceivedListener = () -> { - runOnUiThread(this::checkShizukuPermission); - }; + runOnUiThread(() -> { + if (prefs != null && prefs.getInt(EXEC_MODE_KEY, MODE_NONE) == MODE_SHIZUKU) { + checkShizukuPermission(false); + } + }); + }; // 2. Handle if Shizuku suddenly dies in the background private final Shizuku.OnBinderDeadListener binderDeadListener = () -> { - runOnUiThread(() -> { - statusText.setText("Shizuku is not running."); - statusText.setTextColor(0xFFFF5555); // Red - }); - }; + runOnUiThread(() -> { + if (prefs != null && prefs.getInt(EXEC_MODE_KEY, MODE_NONE) == MODE_SHIZUKU) { + statusText.setText("Shizuku is not running."); + statusText.setTextColor(0xFFFF5555); + } + }); + }; + // 3. React instantly when the user taps "Allow" on the Shizuku Popup private final Shizuku.OnRequestPermissionResultListener permissionResultListener = (requestCode, grantResult) -> { - runOnUiThread(this::checkShizukuPermission); + runOnUiThread(() -> { + if (prefs != null && prefs.getInt(EXEC_MODE_KEY, MODE_NONE) == MODE_SHIZUKU) { + checkShizukuPermission(false); + } + }); }; @Override @@ -55,22 +81,27 @@ protected void onCreate(Bundle savedInstanceState) { Shizuku.addBinderDeadListener(binderDeadListener); Shizuku.addRequestPermissionResultListener(permissionResultListener); - // Load saved mode (Default: Root = 1, Shizuku = 2) - int savedMode = prefs.getInt(EXEC_MODE_KEY, 1); - if (savedMode == 2) { + // Load saved mode (Default =0, Root = 1, Shizuku = 2) + int savedMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); + if (savedMode == MODE_ROOT) { + radioRoot.setChecked(true); + checkRootPermission(); + } else if (savedMode == MODE_SHIZUKU) { radioShizuku.setChecked(true); - checkShizukuPermission(); // Try immediately + checkShizukuPermission(false); } else { - radioRoot.setChecked(true); + radioGroup.clearCheck(); + statusText.setText("Select Root or Shizuku mode."); + statusText.setTextColor(0xFFFFB300); } radioGroup.setOnCheckedChangeListener((group, checkedId) -> { if (checkedId == R.id.radioRoot) { - prefs.edit().putInt(EXEC_MODE_KEY, 1).apply(); - statusText.setText(""); + prefs.edit().putInt(EXEC_MODE_KEY, MODE_ROOT).apply(); + checkRootPermission(); } else if (checkedId == R.id.radioShizuku) { - prefs.edit().putInt(EXEC_MODE_KEY, 2).apply(); - checkShizukuPermission(); + prefs.edit().putInt(EXEC_MODE_KEY, MODE_SHIZUKU).apply(); + checkShizukuPermission(true); } }); } @@ -84,20 +115,63 @@ protected void onDestroy() { Shizuku.removeRequestPermissionResultListener(permissionResultListener); } - private void checkShizukuPermission() { - if (!Shizuku.pingBinder()) { - statusText.setText("Waiting for Shizuku..."); - statusText.setTextColor(0xFFFF5555); // Red - return; - } + private void checkRootPermission() { + statusText.setText("Checking root permission..."); + statusText.setTextColor(0xFFFFB300); + + new Thread(() -> { + boolean granted = false; + + try { + Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", "id"}); + int exitCode = process.waitFor(); + granted = exitCode == 0; + } catch (Exception ignored) { + granted = false; + } + + boolean finalGranted = granted; + runOnUiThread(() -> { + if (prefs == null || prefs.getInt(EXEC_MODE_KEY, MODE_NONE) != MODE_ROOT) { + return; + } - if (Shizuku.checkSelfPermission() != PackageManager.PERMISSION_GRANTED) { - statusText.setText("Requesting Shizuku permission..."); - statusText.setTextColor(0xFFFFB300); // Yellow - Shizuku.requestPermission(0); // This line triggers the Auto-Popup - } else { - statusText.setText("Shizuku mode active & authorized!"); - statusText.setTextColor(0xFF1B873F); // Green - } + if (finalGranted) { + statusText.setText("Root mode active & authorized!"); + statusText.setTextColor(0xFF1B873F); + } else { + statusText.setText("Root permission denied or unavailable."); + statusText.setTextColor(0xFFFF5555); + } + }); + }).start(); } + + private void checkShizukuPermission(boolean requestIfNeeded) { + try { + if (!Shizuku.pingBinder()) { + statusText.setText("Shizuku is not running."); + statusText.setTextColor(0xFFFF5555); + return; + } + + if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { + statusText.setText("Shizuku mode active & authorized!"); + statusText.setTextColor(0xFF1B873F); + return; + } + + statusText.setText("Shizuku permission not granted."); + statusText.setTextColor(0xFFFFB300); + + if (requestIfNeeded) { + statusText.setText("Requesting Shizuku permission..."); + statusText.setTextColor(0xFFFFB300); + Shizuku.requestPermission(0); + } + } catch (Exception e) { + statusText.setText("Shizuku check failed."); + statusText.setTextColor(0xFFFF5555); + } + } } From c460c083b81dceb209872b9a121ce0ca570deaf7 Mon Sep 17 00:00:00 2001 From: Dhangofa <117024745+Dhangofa@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:20:22 +0530 Subject: [PATCH 02/10] Added state constants and cache QS tile icons - Added explicit execution mode constants for none, root, and Shizuku. - Added explicit network state constants including STATE_UNKNOWN. - Updated tile startup to avoid assuming 4G Only when no cached state exists. - Added unknown/unavailable tile state for unconfigured mode. - Added cached QS tile icons to avoid regenerating bitmap icons on every tile update. - Refactored state cycling into getNextState(). - Refactored state-to-bitmask selection into getBinaryForState(). - Prevented tile click from running when no execution mode is selected. - Updated applyNetworkMode() to use execution mode constants instead of defaulting to Root. --- .../networktoggle/NetworkTileService.java | 183 ++++++++++++++---- 1 file changed, 141 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java index a365b55..d77f286 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java +++ b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java @@ -18,40 +18,91 @@ public class NetworkTileService extends TileService { private static final String PREFS_NAME = "NetTogglePrefs"; private static final String STATE_KEY = "net_state"; private static final String EXEC_MODE_KEY = "exec_mode"; - - private static final String BIN_4G_ONLY = "1000000000000"; - private static final String BIN_5G_ONLY = "10000000000000000000"; - private static final String BIN_PREF_5G = "11011111101111111111"; - private static final String BIN_PREF_4G = "1001101001110000111"; + + private static final int MODE_NONE = 0; + private static final int MODE_ROOT = 1; + private static final int MODE_SHIZUKU = 2; + + private static final int STATE_UNKNOWN = 0; + private static final int STATE_4G_ONLY = 1; + private static final int STATE_5G_ONLY = 2; + private static final int STATE_PREF_5G = 3; + private static final int STATE_PREF_4G = 4; + + private static final String BIN_4G_ONLY = "1000000000000"; // Legacy Id 11 (4096) + private static final String BIN_5G_ONLY = "10000000000000000000"; // Legacy Id 23 (524288) + private static final String BIN_PREF_5G = "11011111101111111111"; // Legacy Id 33 (916479) + private static final String BIN_PREF_4G = "1001101001110000111"; // Legacy Id 9 (316295) + + private static Icon ICON_4G; + private static Icon ICON_5G; + private static Icon ICON_P5G; + private static Icon ICON_P4G; + private static Icon ICON_UNKNOWN; @Override public void onStartListening() { super.onStartListening(); SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - updateTileUI(prefs.getInt(STATE_KEY, 1)); + updateTileUI(prefs.getInt(STATE_KEY, STATE_UNKNOWN)); } @Override public void onClick() { super.onClick(); SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - int currentState = prefs.getInt(STATE_KEY, 1); + + int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); + if (execMode == MODE_NONE) { + updateTileUI(STATE_UNKNOWN); + return; + } - int nextState = (currentState % 4) + 1; - String targetBinary = BIN_4G_ONLY; + int currentState = prefs.getInt(STATE_KEY, STATE_UNKNOWN); - switch (nextState) { - case 1: targetBinary = BIN_4G_ONLY; break; - case 2: targetBinary = BIN_5G_ONLY; break; - case 3: targetBinary = BIN_PREF_5G; break; - case 4: targetBinary = BIN_PREF_4G; break; - } + int nextState = getNextState(currentState); + String targetBinary = getBinaryForState(nextState); applyNetworkMode(targetBinary); prefs.edit().putInt(STATE_KEY, nextState).apply(); updateTileUI(nextState); } + + private int getNextState(int currentState) { + switch (currentState) { + case STATE_4G_ONLY: + return STATE_5G_ONLY; + + case STATE_5G_ONLY: + return STATE_PREF_5G; + + case STATE_PREF_5G: + return STATE_PREF_4G; + + case STATE_PREF_4G: + case STATE_UNKNOWN: + default: + return STATE_4G_ONLY; + } + } + + private String getBinaryForState(int state) { + switch (state) { + case STATE_5G_ONLY: + return BIN_5G_ONLY; + + case STATE_PREF_5G: + return BIN_PREF_5G; + + case STATE_PREF_4G: + return BIN_PREF_4G; + + case STATE_4G_ONLY: + default: + return BIN_4G_ONLY; + } + } private Icon createTextOnlyIcon(String text) { int size = 256; @@ -76,39 +127,87 @@ private Icon createTextOnlyIcon(String text) { return Icon.createWithBitmap(bitmap); } + + private Icon getCachedIcon(String text) { + switch (text) { + case "4G": + if (ICON_4G == null) { + ICON_4G = createTextOnlyIcon("4G"); + } + return ICON_4G; + + case "5G": + if (ICON_5G == null) { + ICON_5G = createTextOnlyIcon("5G"); + } + return ICON_5G; + + case "P5G": + if (ICON_P5G == null) { + ICON_P5G = createTextOnlyIcon("P5G"); + } + return ICON_P5G; + + case "P4G": + if (ICON_P4G == null) { + ICON_P4G = createTextOnlyIcon("P4G"); + } + return ICON_P4G; + + default: + if (ICON_UNKNOWN == null) { + ICON_UNKNOWN = createTextOnlyIcon("?"); + } + return ICON_UNKNOWN; + } + } private void updateTileUI(int state) { - Tile tile = getQsTile(); - if (tile == null) return; - tile.setState(Tile.STATE_ACTIVE); - - switch (state) { - case 1: - tile.setLabel("4G Only"); - tile.setIcon(createTextOnlyIcon("4G")); - break; - case 2: - tile.setLabel("5G Only"); - tile.setIcon(createTextOnlyIcon("5G")); - break; - case 3: - tile.setLabel("Pref 5G"); - tile.setIcon(createTextOnlyIcon("P5G")); - break; - case 4: - tile.setLabel("Pref 4G"); - tile.setIcon(createTextOnlyIcon("P4G")); - break; - } - tile.updateTile(); - } + Tile tile = getQsTile(); + if (tile == null) return; + + switch (state) { + case STATE_4G_ONLY: + tile.setState(Tile.STATE_ACTIVE); + tile.setLabel("4G Only"); + tile.setIcon(getCachedIcon("4G")); + break; + + case STATE_5G_ONLY: + tile.setState(Tile.STATE_ACTIVE); + tile.setLabel("5G Only"); + tile.setIcon(getCachedIcon("5G")); + break; + + case STATE_PREF_5G: + tile.setState(Tile.STATE_ACTIVE); + tile.setLabel("Pref 5G"); + tile.setIcon(getCachedIcon("P5G")); + break; + + case STATE_PREF_4G: + tile.setState(Tile.STATE_ACTIVE); + tile.setLabel("Pref 4G"); + tile.setIcon(getCachedIcon("P4G")); + break; + + case STATE_UNKNOWN: + default: + tile.setState(Tile.STATE_UNAVAILABLE); + tile.setLabel("Net Mode ?"); + tile.setIcon(getCachedIcon("?")); + break; + } + + tile.updateTile(); + } private void applyNetworkMode(String binaryString) { SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - int execMode = prefs.getInt(EXEC_MODE_KEY, 1); // 1 = Root, 2 = Shizuku + int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); String command = "cmd phone set-allowed-network-types-for-users -s 0 " + binaryString; - if (execMode == 2) { + if (execMode == MODE_SHIZUKU) { // Shizuku Execution Method via Reflection try { if (Shizuku.pingBinder() && Shizuku.checkSelfPermission() == android.content.pm.PackageManager.PERMISSION_GRANTED) { @@ -125,7 +224,7 @@ private void applyNetworkMode(String binaryString) { } catch (Exception e) { e.printStackTrace(); } - } else { + } else if (execMode == MODE_ROOT) { // Standard Root Execution Method try { Process process = Runtime.getRuntime().exec("su"); From eb4555347cc2f93ec3266e3eeee290f3c1d1ae3d Mon Sep 17 00:00:00 2001 From: Dhangofa <117024745+Dhangofa@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:13:08 +0530 Subject: [PATCH 03/10] Verify command success before updating tile state - Changed applyNetworkMode() to return success or failure. - Updated QS tile click flow to save and display the next state only after successful command execution. - Added separate Root and Shizuku command runners. - Simplified Root execution using `su -c`. - Added exit code checks for both Root and Shizuku command execution. - Prevented tile state from changing visually when command execution fails. - Preserved previous tile state on failed toggle attempt. - Removed old DataOutputStream-based Root command flow. --- .../networktoggle/NetworkTileService.java | 127 ++++++++++++------ 1 file changed, 85 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java index d77f286..e047fee 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java +++ b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java @@ -9,7 +9,6 @@ import android.graphics.drawable.Icon; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; -import java.io.DataOutputStream; import java.lang.reflect.Method; import rikka.shizuku.Shizuku; @@ -63,10 +62,14 @@ public void onClick() { int nextState = getNextState(currentState); String targetBinary = getBinaryForState(nextState); - applyNetworkMode(targetBinary); + boolean success = applyNetworkMode(targetBinary); - prefs.edit().putInt(STATE_KEY, nextState).apply(); - updateTileUI(nextState); + if (success) { + prefs.edit().putInt(STATE_KEY, nextState).apply(); + updateTileUI(nextState); + } else { + updateTileUI(currentState); + } } private int getNextState(int currentState) { @@ -193,49 +196,89 @@ private void updateTileUI(int state) { case STATE_UNKNOWN: default: - tile.setState(Tile.STATE_UNAVAILABLE); - tile.setLabel("Net Mode ?"); + SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); + + if (execMode == MODE_NONE) { + tile.setState(Tile.STATE_UNAVAILABLE); + tile.setLabel("Setup Required"); + } else { + tile.setState(Tile.STATE_INACTIVE); + tile.setLabel("Tap to Set 4G"); + } + tile.setIcon(getCachedIcon("?")); break; } tile.updateTile(); } + + private boolean applyNetworkMode(String binaryString) { + SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); - private void applyNetworkMode(String binaryString) { - SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); - String command = "cmd phone set-allowed-network-types-for-users -s 0 " + binaryString; - - if (execMode == MODE_SHIZUKU) { - // Shizuku Execution Method via Reflection - try { - if (Shizuku.pingBinder() && Shizuku.checkSelfPermission() == android.content.pm.PackageManager.PERMISSION_GRANTED) { - - // Uses Reflection to bypass the private access restriction on Shizuku.newProcess - Method newProcessMethod = Shizuku.class.getDeclaredMethod("newProcess", String[].class, String[].class, String.class); - newProcessMethod.setAccessible(true); - - Process process = (Process) newProcessMethod.invoke(null, new String[]{"sh", "-c", command}, null, null); - if (process != null) { - process.waitFor(); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } else if (execMode == MODE_ROOT) { - // Standard Root Execution Method - try { - Process process = Runtime.getRuntime().exec("su"); - DataOutputStream os = new DataOutputStream(process.getOutputStream()); - os.writeBytes(command + "\n"); - os.writeBytes("exit\n"); - os.flush(); - process.waitFor(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } + if (execMode == MODE_NONE) { + return false; + } + + String command = "cmd phone set-allowed-network-types-for-users -s 0 " + binaryString; + + if (execMode == MODE_SHIZUKU) { + return runCommandWithShizuku(command); + } else if (execMode == MODE_ROOT) { + return runCommandWithRoot(command); + } + + return false; + } + + private boolean runCommandWithRoot(String command) { + try { + Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", command}); + int exitCode = process.waitFor(); + return exitCode == 0; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + private boolean runCommandWithShizuku(String command) { + try { + if (!Shizuku.pingBinder()) { + return false; + } + + if (Shizuku.checkSelfPermission() != android.content.pm.PackageManager.PERMISSION_GRANTED) { + return false; + } + + Method newProcessMethod = Shizuku.class.getDeclaredMethod( + "newProcess", + String[].class, + String[].class, + String.class + ); + newProcessMethod.setAccessible(true); + + Process process = (Process) newProcessMethod.invoke( + null, + new String[]{"sh", "-c", command}, + null, + null + ); + + if (process == null) { + return false; + } + + int exitCode = process.waitFor(); + return exitCode == 0; + + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } } From 3d36c464c9efdfdb5512ba66ccb6f0b04dbdc5f4 Mon Sep 17 00:00:00 2001 From: Dhangofa <117024745+Dhangofa@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:26:32 +0530 Subject: [PATCH 04/10] Used default data subscription for network mode command - Added default data subscription lookup using SubscriptionManager. - Replaced hardcoded `-s 0` with the current default data subscription ID. - Added invalid subscription handling using SubscriptionManager.INVALID_SUBSCRIPTION_ID. - Prevented network mode command execution when no valid data subscription is available. - Improved dual-SIM compatibility by targeting the active/default data SIM instead of assuming subscription 0. - Kept existing binary allowed-network-type values because the command path supports them on tested devices. --- .../networktoggle/NetworkTileService.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java index e047fee..b006617 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java +++ b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java @@ -9,6 +9,7 @@ import android.graphics.drawable.Icon; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; +import android.telephony.SubscriptionManager; import java.lang.reflect.Method; import rikka.shizuku.Shizuku; @@ -28,10 +29,10 @@ public class NetworkTileService extends TileService { private static final int STATE_PREF_5G = 3; private static final int STATE_PREF_4G = 4; - private static final String BIN_4G_ONLY = "1000000000000"; // Legacy Id 11 (4096) - private static final String BIN_5G_ONLY = "10000000000000000000"; // Legacy Id 23 (524288) - private static final String BIN_PREF_5G = "11011111101111111111"; // Legacy Id 33 (916479) - private static final String BIN_PREF_4G = "1001101001110000111"; // Legacy Id 9 (316295) + private static final String BIN_4G_ONLY = "1000000000000"; // Legacy Id 11, bitmask 4096 + private static final String BIN_5G_ONLY = "10000000000000000000"; // Legacy Id 23, bitmask 524288 + private static final String BIN_PREF_5G = "11011111101111111111"; // Legacy Id 33, bitmask 916479 + private static final String BIN_PREF_4G = "1001101001110000111"; // Legacy Id 9 , bitmask 316295 private static Icon ICON_4G; private static Icon ICON_5G; @@ -214,6 +215,10 @@ private void updateTileUI(int state) { tile.updateTile(); } + private int getDefaultDataSubId() { + return SubscriptionManager.getDefaultDataSubscriptionId(); + } + private boolean applyNetworkMode(String binaryString) { SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); @@ -222,7 +227,13 @@ private boolean applyNetworkMode(String binaryString) { return false; } - String command = "cmd phone set-allowed-network-types-for-users -s 0 " + binaryString; + int subId = getDefaultDataSubId(); + + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + return false; + } + + String command = "cmd phone set-allowed-network-types-for-users -s " + subId + " " + binaryString; if (execMode == MODE_SHIZUKU) { return runCommandWithShizuku(command); From c4ffab82733b4ba777e010c71f389aa0a696984a Mon Sep 17 00:00:00 2001 From: Dhangofa <117024745+Dhangofa@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:35:36 +0530 Subject: [PATCH 05/10] run network toggle command asynchronously - Added a single-thread executor for QS tile command execution. - Moved root/Shizuku network mode command execution off the tile click callback path. - Added immediate "Switching..." tile feedback while the command runs. - Updated tile state on the main executor after command completion. - Preserved previous tile state when command execution fails. - Prevented the QS tile from blocking while waiting for root/Shizuku process completion. --- .../networktoggle/NetworkTileService.java | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java index b006617..121d4b3 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java +++ b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java @@ -11,6 +11,8 @@ import android.service.quicksettings.TileService; import android.telephony.SubscriptionManager; import java.lang.reflect.Method; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import rikka.shizuku.Shizuku; public class NetworkTileService extends TileService { @@ -34,6 +36,8 @@ public class NetworkTileService extends TileService { private static final String BIN_PREF_5G = "11011111101111111111"; // Legacy Id 33, bitmask 916479 private static final String BIN_PREF_4G = "1001101001110000111"; // Legacy Id 9 , bitmask 316295 + private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(); + private static Icon ICON_4G; private static Icon ICON_5G; private static Icon ICON_P5G; @@ -46,32 +50,39 @@ public void onStartListening() { SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); updateTileUI(prefs.getInt(STATE_KEY, STATE_UNKNOWN)); } + + @Override + public void onClick() { + super.onClick(); + + SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - @Override - public void onClick() { - super.onClick(); - SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); if (execMode == MODE_NONE) { updateTileUI(STATE_UNKNOWN); return; } - int currentState = prefs.getInt(STATE_KEY, STATE_UNKNOWN); + int currentState = prefs.getInt(STATE_KEY, STATE_UNKNOWN); - int nextState = getNextState(currentState); + int nextState = getNextState(currentState); String targetBinary = getBinaryForState(nextState); - boolean success = applyNetworkMode(targetBinary); + updateTileSwitchingUI(); - if (success) { - prefs.edit().putInt(STATE_KEY, nextState).apply(); - updateTileUI(nextState); - } else { - updateTileUI(currentState); - } - } + EXECUTOR.execute(() -> { + boolean success = applyNetworkMode(targetBinary); + + getMainExecutor().execute(() -> { + if (success) { + prefs.edit().putInt(STATE_KEY, nextState).apply(); + updateTileUI(nextState); + } else { + updateTileUI(currentState); + } + }); + }); + } private int getNextState(int currentState) { switch (currentState) { @@ -165,6 +176,16 @@ private Icon getCachedIcon(String text) { return ICON_UNKNOWN; } } + + private void updateTileSwitchingUI() { + Tile tile = getQsTile(); + if (tile == null) return; + + tile.setState(Tile.STATE_INACTIVE); + tile.setLabel("Switching..."); + tile.setIcon(getCachedIcon("?")); + tile.updateTile(); + } private void updateTileUI(int state) { Tile tile = getQsTile(); From 7adc012c77f22b9a8befbe75e584d7c962ef3a3e Mon Sep 17 00:00:00 2001 From: Dhangofa <117024745+Dhangofa@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:42:37 +0530 Subject: [PATCH 06/10] Improved Android 7 compatibility and prevent duplicate toggles - Replaced API 28+ getMainExecutor() usage with Handler and Looper.getMainLooper() for Android 7+ compatibility. - Added AtomicBoolean switching guard to prevent rapid QS tile taps from queueing duplicate network mode commands. - Kept network mode command execution on a background single-thread executor. - Posted tile UI updates back to the main thread after command completion. - Preserved previous tile state when command execution fails. - Improved QS tile stability across Android 7 through newer Android versions. --- .../networktoggle/NetworkTileService.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java index 121d4b3..396b97b 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java +++ b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java @@ -10,6 +10,9 @@ import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; import android.telephony.SubscriptionManager; +import android.os.Handler; +import android.os.Looper; +import java.util.concurrent.atomic.AtomicBoolean; import java.lang.reflect.Method; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -37,6 +40,9 @@ public class NetworkTileService extends TileService { private static final String BIN_PREF_4G = "1001101001110000111"; // Legacy Id 9 , bitmask 316295 private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(); + private static final AtomicBoolean IS_SWITCHING = new AtomicBoolean(false); + + private final Handler mainHandler = new Handler(Looper.getMainLooper()); private static Icon ICON_4G; private static Icon ICON_5G; @@ -55,10 +61,16 @@ public void onStartListening() { public void onClick() { super.onClick(); + if (!IS_SWITCHING.compareAndSet(false, true)) { + updateTileSwitchingUI(); + return; + } + SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); if (execMode == MODE_NONE) { + IS_SWITCHING.set(false); updateTileUI(STATE_UNKNOWN); return; } @@ -73,12 +85,16 @@ public void onClick() { EXECUTOR.execute(() -> { boolean success = applyNetworkMode(targetBinary); - getMainExecutor().execute(() -> { - if (success) { - prefs.edit().putInt(STATE_KEY, nextState).apply(); - updateTileUI(nextState); - } else { - updateTileUI(currentState); + mainHandler.post(() -> { + try { + if (success) { + prefs.edit().putInt(STATE_KEY, nextState).apply(); + updateTileUI(nextState); + } else { + updateTileUI(currentState); + } + } finally { + IS_SWITCHING.set(false); } }); }); From 3183fd6aeb2493d0b64a90d67ae098519cc12f1d Mon Sep 17 00:00:00 2001 From: Dhangofa <117024745+Dhangofa@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:35:43 +0530 Subject: [PATCH 07/10] Reduced QS refresh overhead and simplify SIM handling - Avoided running real network mode checks on every QS panel open. - Updated onStartListening() to show cached tile state immediately. - Queried real current network mode only when cached state is unknown. - Used `settings get global multi_sim_data_call` as the active data SIM source. - Read current legacy network mode from `preferred_network_mode1/2` based on active data SIM. - Mapped active data SIM index to phone command index for network mode updates. - multi_sim_data_call 1 -> cmd phone -s 0 - multi_sim_data_call 2 -> cmd phone -s 1 - Removed SubscriptionManager-based data subscription lookup from tile command flow. - Added legacy network mode ID mapping for tile state detection. - 11 -> 4G Only - 23 -> 5G Only - 33 -> Preferred 5G - 9 -> Preferred 4G - Simplified current mode detection using preferred_network_mode legacy values. - Cached Shizuku `newProcess` reflection Method to avoid repeated method lookup. - Cached number regex Pattern used for legacy mode parsing. - Simplified CommandResult by removing unused stderr storage. - Added process cleanup in Root and Shizuku command execution paths. - Kept command execution asynchronous and protected by duplicate-toggle guard. - Reduced shell/Shizuku process usage during normal QS tile refresh. --- .../networktoggle/NetworkTileService.java | 269 ++++++++++++++++-- 1 file changed, 238 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java index 396b97b..c96f9db 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java +++ b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java @@ -9,13 +9,17 @@ import android.graphics.drawable.Icon; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; -import android.telephony.SubscriptionManager; import android.os.Handler; import android.os.Looper; import java.util.concurrent.atomic.AtomicBoolean; import java.lang.reflect.Method; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import rikka.shizuku.Shizuku; public class NetworkTileService extends TileService { @@ -37,25 +41,61 @@ public class NetworkTileService extends TileService { private static final String BIN_4G_ONLY = "1000000000000"; // Legacy Id 11, bitmask 4096 private static final String BIN_5G_ONLY = "10000000000000000000"; // Legacy Id 23, bitmask 524288 private static final String BIN_PREF_5G = "11011111101111111111"; // Legacy Id 33, bitmask 916479 - private static final String BIN_PREF_4G = "1001101001110000111"; // Legacy Id 9 , bitmask 316295 + private static final String BIN_PREF_4G = "1001101001110000111"; // Legacy Id 9, bitmask 316295 + + private static final int LEGACY_4G_ONLY = 11; + private static final int LEGACY_5G_ONLY = 23; + private static final int LEGACY_PREF_5G = 33; + private static final int LEGACY_PREF_4G = 9; private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(); private static final AtomicBoolean IS_SWITCHING = new AtomicBoolean(false); + private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+"); + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private static Method SHIZUKU_NEW_PROCESS_METHOD; + private static Icon ICON_4G; private static Icon ICON_5G; private static Icon ICON_P5G; private static Icon ICON_P4G; private static Icon ICON_UNKNOWN; - @Override - public void onStartListening() { - super.onStartListening(); - SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - updateTileUI(prefs.getInt(STATE_KEY, STATE_UNKNOWN)); - } + @Override + public void onStartListening() { + super.onStartListening(); + + SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + int cachedState = prefs.getInt(STATE_KEY, STATE_UNKNOWN); + + updateTileUI(cachedState); + + int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); + if (execMode == MODE_NONE) { + return; + } + + // Lightweight behavior: + // Only read real system mode if we do not have a cached state yet. + if (cachedState != STATE_UNKNOWN) { + return; + } + + EXECUTOR.execute(() -> { + int realState = readCurrentNetworkState(); + + mainHandler.post(() -> { + if (realState != STATE_UNKNOWN) { + prefs.edit().putInt(STATE_KEY, realState).apply(); + updateTileUI(realState); + } else { + updateTileUI(cachedState); + } + }); + }); + } @Override public void onClick() { @@ -134,7 +174,31 @@ private String getBinaryForState(int state) { return BIN_4G_ONLY; } } + + private static Method getShizukuNewProcessMethod() throws NoSuchMethodException { + if (SHIZUKU_NEW_PROCESS_METHOD == null) { + SHIZUKU_NEW_PROCESS_METHOD = Shizuku.class.getDeclaredMethod( + "newProcess", + String[].class, + String[].class, + String.class + ); + SHIZUKU_NEW_PROCESS_METHOD.setAccessible(true); + } + return SHIZUKU_NEW_PROCESS_METHOD; + } + + private static class CommandResult { + final int exitCode; + final String stdout; + + CommandResult(int exitCode, String stdout) { + this.exitCode = exitCode; + this.stdout = stdout; + } + } + private Icon createTextOnlyIcon(String text) { int size = 256; Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); @@ -252,25 +316,164 @@ private void updateTileUI(int state) { tile.updateTile(); } - private int getDefaultDataSubId() { - return SubscriptionManager.getDefaultDataSubscriptionId(); - } - - private boolean applyNetworkMode(String binaryString) { + private int readCurrentNetworkState() { SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); if (execMode == MODE_NONE) { - return false; + return STATE_UNKNOWN; + } + + String command = + "data_sim=$(settings get global multi_sim_data_call); " + + "[ \"$data_sim\" -gt 0 ] 2>/dev/null || exit 1; " + + "settings get global preferred_network_mode${data_sim}"; + + CommandResult result; + + if (execMode == MODE_SHIZUKU) { + result = runCommandForResultWithShizuku(command); + } else if (execMode == MODE_ROOT) { + result = runCommandForResultWithRoot(command); + } else { + return STATE_UNKNOWN; + } + + if (result.exitCode != 0) { + return STATE_UNKNOWN; } - int subId = getDefaultDataSubId(); + return mapLegacyNetworkModeToState(result.stdout); + } + + private CommandResult runCommandForResultWithRoot(String command) { + Process process = null; + + try { + process = Runtime.getRuntime().exec(new String[]{"su", "-c", command}); + + int exitCode = process.waitFor(); + String stdout = readStream(process.getInputStream()); + + return new CommandResult(exitCode, stdout); + } catch (Exception e) { + return new CommandResult(-1, ""); + } finally { + if (process != null) { + process.destroy(); + } + } + } + + private CommandResult runCommandForResultWithShizuku(String command) { + Process process = null; - if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + try { + if (!Shizuku.pingBinder()) { + return new CommandResult(-1, ""); + } + + if (Shizuku.checkSelfPermission() != android.content.pm.PackageManager.PERMISSION_GRANTED) { + return new CommandResult(-1, ""); + } + + process = (Process) getShizukuNewProcessMethod().invoke( + null, + new String[]{"sh", "-c", command}, + null, + null + ); + + if (process == null) { + return new CommandResult(-1, ""); + } + + int exitCode = process.waitFor(); + String stdout = readStream(process.getInputStream()); + + return new CommandResult(exitCode, stdout); + } catch (Exception e) { + return new CommandResult(-1, ""); + } finally { + if (process != null) { + process.destroy(); + } + } + } + + private String readStream(InputStream inputStream) { + StringBuilder builder = new StringBuilder(); + + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + String line; + + while ((line = reader.readLine()) != null) { + builder.append(line).append('\n'); + } + } catch (Exception ignored) { + } + + return builder.toString().trim(); + } + + private Integer extractFirstInt(String text) { + try { + if (text == null || text.trim().isEmpty() || text.trim().equalsIgnoreCase("null")) { + return null; + } + + Matcher matcher = NUMBER_PATTERN.matcher(text); + + if (matcher.find()) { + return Integer.parseInt(matcher.group()); + } + + return null; + } catch (Exception e) { + return null; + } + } + + private int mapLegacyNetworkModeToState(String output) { + Integer legacyMode = extractFirstInt(output); + + if (legacyMode == null) { + return STATE_UNKNOWN; + } + + switch (legacyMode) { + case LEGACY_4G_ONLY: + return STATE_4G_ONLY; + + case LEGACY_5G_ONLY: + return STATE_5G_ONLY; + + case LEGACY_PREF_5G: + return STATE_PREF_5G; + + case LEGACY_PREF_4G: + return STATE_PREF_4G; + + default: + return STATE_UNKNOWN; + } + } + + + private boolean applyNetworkMode(String binaryString) { + SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); + + if (execMode == MODE_NONE) { return false; } - String command = "cmd phone set-allowed-network-types-for-users -s " + subId + " " + binaryString; + String command = + "data_sim=$(settings get global multi_sim_data_call); " + + "[ \"$data_sim\" -gt 0 ] 2>/dev/null || exit 1; " + + "phone_index=$((data_sim - 1)); " + + "cmd phone set-allowed-network-types-for-users -s \"$phone_index\" " + binaryString; if (execMode == MODE_SHIZUKU) { return runCommandWithShizuku(command); @@ -282,17 +485,25 @@ private boolean applyNetworkMode(String binaryString) { } private boolean runCommandWithRoot(String command) { + Process process = null; + try { - Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", command}); + process = Runtime.getRuntime().exec(new String[]{"su", "-c", command}); int exitCode = process.waitFor(); return exitCode == 0; } catch (Exception e) { e.printStackTrace(); return false; + } finally { + if (process != null) { + process.destroy(); + } } } private boolean runCommandWithShizuku(String command) { + Process process = null; + try { if (!Shizuku.pingBinder()) { return false; @@ -302,19 +513,11 @@ private boolean runCommandWithShizuku(String command) { return false; } - Method newProcessMethod = Shizuku.class.getDeclaredMethod( - "newProcess", - String[].class, - String[].class, - String.class - ); - newProcessMethod.setAccessible(true); - - Process process = (Process) newProcessMethod.invoke( - null, - new String[]{"sh", "-c", command}, - null, - null + process = (Process) getShizukuNewProcessMethod().invoke( + null, + new String[]{"sh", "-c", command}, + null, + null ); if (process == null) { @@ -327,6 +530,10 @@ private boolean runCommandWithShizuku(String command) { } catch (Exception e) { e.printStackTrace(); return false; + }finally { + if (process != null) { + process.destroy(); + } } } } From f5fac6cfa1052f4b80d54193e8ff3cf2a3058444 Mon Sep 17 00:00:00 2001 From: Dhangofa <117024745+Dhangofa@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:43:58 +0530 Subject: [PATCH 08/10] GitHub Actions to auto-increment versionCode change --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c27905b..b9b8ed6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,7 +10,7 @@ android { applicationId = "com.dhangofa.networktoggle" minSdk = 24 targetSdk = 36 - versionCode = 2 + versionCode = (project.findProperty("versionCode") as String?)?.toIntOrNull() ?: 2 versionName = "1.0" } // Suggested by IzzyOnDroid From 8998cb6b93f2c906ab515b79f9943daab1728418 Mon Sep 17 00:00:00 2001 From: Dhangofa <117024745+Dhangofa@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:45:46 +0530 Subject: [PATCH 09/10] Update proguard-rules.pro --- app/proguard-rules.pro | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 67b7c78..e4fe6a3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,2 +1,6 @@ # Keep the Quick Settings TileService intact during R8/Proguard minification -keep class com.dhangofa.networktoggle.NetworkTileService { *; } +-keep class rikka.shizuku.** { *; } +-keep class moe.shizuku.** { *; } +-dontwarn rikka.shizuku.** +-dontwarn moe.shizuku.** From 1cdd7b9a75335fd28282386ec7a1dce2fe0a858a Mon Sep 17 00:00:00 2001 From: Dhangofa <117024745+Dhangofa@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:59:52 +0530 Subject: [PATCH 10/10] Cleaned up permission checks on destroy - Added Activity destroyed flag to prevent stale UI updates after MainActivity closes. - Tracked Root permission check thread and process. - Destroyed active Root check process during onDestroy(). - Interrupted active Root check thread during Activity cleanup. - Preserved Shizuku listener cleanup in onDestroy(). - Guarded Shizuku callbacks so they update UI only when Activity is alive and Shizuku mode is selected. - Prevented stale Root permission check result from updating UI after Activity destruction. --- .../dhangofa/networktoggle/MainActivity.java | 161 +++++++++++------- 1 file changed, 104 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java index 8977805..3fc5a79 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java +++ b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java @@ -34,40 +34,51 @@ public class MainActivity extends Activity { private RadioButton radioShizuku; private TextView statusText; private SharedPreferences prefs; - + + private volatile boolean activityDestroyed = false; + private Thread rootCheckThread; + private Process rootCheckProcess; + // 1. First check Shizuku mode selected then wait for Shizuku Binder to be injected, then check permissions private final Shizuku.OnBinderReceivedListener binderReceivedListener = () -> { - runOnUiThread(() -> { - if (prefs != null && prefs.getInt(EXEC_MODE_KEY, MODE_NONE) == MODE_SHIZUKU) { - checkShizukuPermission(false); - } - }); + runOnUiThread(() -> { + if (!activityDestroyed + && prefs != null + && prefs.getInt(EXEC_MODE_KEY, MODE_NONE) == MODE_SHIZUKU) { + checkShizukuPermission(false); + } + }); }; // 2. Handle if Shizuku suddenly dies in the background private final Shizuku.OnBinderDeadListener binderDeadListener = () -> { - runOnUiThread(() -> { - if (prefs != null && prefs.getInt(EXEC_MODE_KEY, MODE_NONE) == MODE_SHIZUKU) { - statusText.setText("Shizuku is not running."); - statusText.setTextColor(0xFFFF5555); - } - }); + runOnUiThread(() -> { + if (!activityDestroyed + && prefs != null + && prefs.getInt(EXEC_MODE_KEY, MODE_NONE) == MODE_SHIZUKU) { + statusText.setText("Shizuku is not running."); + statusText.setTextColor(0xFFFF5555); + } + }); }; // 3. React instantly when the user taps "Allow" on the Shizuku Popup private final Shizuku.OnRequestPermissionResultListener permissionResultListener = (requestCode, grantResult) -> { - runOnUiThread(() -> { - if (prefs != null && prefs.getInt(EXEC_MODE_KEY, MODE_NONE) == MODE_SHIZUKU) { - checkShizukuPermission(false); - } - }); - }; + runOnUiThread(() -> { + if (!activityDestroyed + && prefs != null + && prefs.getInt(EXEC_MODE_KEY, MODE_NONE) == MODE_SHIZUKU) { + checkShizukuPermission(false); + } + }); + }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); + activityDestroyed = false; + setContentView(R.layout.activity_main); prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); @@ -106,48 +117,84 @@ protected void onCreate(Bundle savedInstanceState) { }); } - @Override - protected void onDestroy() { - super.onDestroy(); - // Prevent memory leaks by destroying the listeners when the app closes - Shizuku.removeBinderReceivedListener(binderReceivedListener); - Shizuku.removeBinderDeadListener(binderDeadListener); - Shizuku.removeRequestPermissionResultListener(permissionResultListener); - } + @Override + protected void onDestroy() { + activityDestroyed = true; + + // Prevent memory leaks by destroying Shizuku listeners when the app closes + Shizuku.removeBinderReceivedListener(binderReceivedListener); + Shizuku.removeBinderDeadListener(binderDeadListener); + Shizuku.removeRequestPermissionResultListener(permissionResultListener); + + // Stop any running root permission check process + if (rootCheckProcess != null) { + rootCheckProcess.destroy(); + rootCheckProcess = null; + } + + // Interrupt root check thread if it is still active + if (rootCheckThread != null && rootCheckThread.isAlive()) { + rootCheckThread.interrupt(); + rootCheckThread = null; + } + + super.onDestroy(); + } private void checkRootPermission() { - statusText.setText("Checking root permission..."); - statusText.setTextColor(0xFFFFB300); - - new Thread(() -> { - boolean granted = false; - - try { - Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", "id"}); - int exitCode = process.waitFor(); - granted = exitCode == 0; - } catch (Exception ignored) { - granted = false; - } - - boolean finalGranted = granted; - runOnUiThread(() -> { - if (prefs == null || prefs.getInt(EXEC_MODE_KEY, MODE_NONE) != MODE_ROOT) { - return; - } - - if (finalGranted) { - statusText.setText("Root mode active & authorized!"); - statusText.setTextColor(0xFF1B873F); - } else { - statusText.setText("Root permission denied or unavailable."); - statusText.setTextColor(0xFFFF5555); - } - }); - }).start(); - } + statusText.setText("Checking root permission..."); + statusText.setTextColor(0xFFFFB300); + + rootCheckThread = new Thread(() -> { + boolean granted = false; + Process process = null; + + try { + process = Runtime.getRuntime().exec(new String[]{"su", "-c", "id"}); + rootCheckProcess = process; + + int exitCode = process.waitFor(); + granted = exitCode == 0; + } catch (Exception ignored) { + granted = false; + } finally { + if (process != null) { + process.destroy(); + } + + if (rootCheckProcess == process) { + rootCheckProcess = null; + } + } + + boolean finalGranted = granted; + + runOnUiThread(() -> { + if (activityDestroyed) { + return; + } + + if (prefs == null || prefs.getInt(EXEC_MODE_KEY, MODE_NONE) != MODE_ROOT) { + return; + } + + if (finalGranted) { + statusText.setText("Root mode active & authorized!"); + statusText.setTextColor(0xFF1B873F); + } else { + statusText.setText("Root permission denied or unavailable."); + statusText.setTextColor(0xFFFF5555); + } + }); + }); + + rootCheckThread.start(); + } private void checkShizukuPermission(boolean requestIfNeeded) { + if (activityDestroyed) { + return; + } try { if (!Shizuku.pingBinder()) { statusText.setText("Shizuku is not running.");