From a2edafe063be3d1eea68177d6ded05b17909505c Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:05:16 +0530 Subject: [PATCH 01/10] Added a shared CommandExecutor interface --- .../dhangofa/networktoggle/command/CommandExecutor.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutor.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutor.java b/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutor.java new file mode 100644 index 0000000..66cbeab --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutor.java @@ -0,0 +1,7 @@ +package com.dhangofa.networktoggle.command; + +import com.dhangofa.networktoggle.model.CommandResult; + +public interface CommandExecutor { + CommandResult execute(String command); +} From 007b695472d0afc859ee8980bb80d1eeb0412ce7 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:06:49 +0530 Subject: [PATCH 02/10] Added CommandExecutorFactory for execution-mode selection --- .../command/CommandExecutorFactory.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutorFactory.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutorFactory.java b/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutorFactory.java new file mode 100644 index 0000000..150a4d3 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutorFactory.java @@ -0,0 +1,16 @@ +package com.dhangofa.networktoggle.command; + +import com.dhangofa.networktoggle.model.ExecutionMode; + +public final class CommandExecutorFactory { + private static final CommandExecutor ROOT = new RootCommandExecutor(); + private static final CommandExecutor SHIZUKU = new ShizukuCommandExecutor(); + + private CommandExecutorFactory() {} + + public static CommandExecutor forMode(ExecutionMode mode) { + if (mode == ExecutionMode.ROOT) return ROOT; + if (mode == ExecutionMode.SHIZUKU) return SHIZUKU; + return null; + } +} From faad2e41bbf08a284c4b13f7897a0174b89d4656 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:09:29 +0530 Subject: [PATCH 03/10] Added CommandResult with command, exit code, stdout, stderr, and exception details --- .../networktoggle/command/CommandResult.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/command/CommandResult.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/CommandResult.java b/app/src/main/java/com/dhangofa/networktoggle/command/CommandResult.java new file mode 100644 index 0000000..1997515 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/command/CommandResult.java @@ -0,0 +1,32 @@ +package com.dhangofa.networktoggle.model; + +public final class CommandResult { + private final String command; + private final int exitCode; + private final String stdout; + private final String stderr; + private final String exceptionMessage; + + private CommandResult(String command, int exitCode, String stdout, String stderr, String exceptionMessage) { + this.command = command == null ? "" : command; + this.exitCode = exitCode; + this.stdout = stdout == null ? "" : stdout; + this.stderr = stderr == null ? "" : stderr; + this.exceptionMessage = exceptionMessage == null ? "" : exceptionMessage; + } + + public static CommandResult completed(String command, int exitCode, String stdout, String stderr) { + return new CommandResult(command, exitCode, stdout, stderr, ""); + } + + public static CommandResult failed(String command, String message) { + return new CommandResult(command, -1, "", "", message); + } + + public boolean isSuccess() { return exitCode == 0; } + public String getCommand() { return command; } + public int getExitCode() { return exitCode; } + public String getStdout() { return stdout; } + public String getStderr() { return stderr; } + public String getExceptionMessage() { return exceptionMessage; } +} From a314dc30bfac1905ab1f9742fde60f4d0304e413 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:12:22 +0530 Subject: [PATCH 04/10] Changed the location to model folder --- .../dhangofa/networktoggle/{command => model}/CommandResult.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/dhangofa/networktoggle/{command => model}/CommandResult.java (100%) diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/CommandResult.java b/app/src/main/java/com/dhangofa/networktoggle/model/CommandResult.java similarity index 100% rename from app/src/main/java/com/dhangofa/networktoggle/command/CommandResult.java rename to app/src/main/java/com/dhangofa/networktoggle/model/CommandResult.java From 9f018418f95f764859422bd84adb846272141def Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:15:15 +0530 Subject: [PATCH 05/10] Added concurrent stdout and stderr stream collection --- .../command/ProcessResultReader.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/command/ProcessResultReader.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/ProcessResultReader.java b/app/src/main/java/com/dhangofa/networktoggle/command/ProcessResultReader.java new file mode 100644 index 0000000..3397b81 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/command/ProcessResultReader.java @@ -0,0 +1,18 @@ +package com.dhangofa.networktoggle.command; + +import com.dhangofa.networktoggle.model.CommandResult; + +final class ProcessResultReader { + private ProcessResultReader() {} + + static CommandResult collect(String command, Process process) throws Exception { + StreamCollector stdout = new StreamCollector("NetToggle-stdout", process.getInputStream()); + StreamCollector stderr = new StreamCollector("NetToggle-stderr", process.getErrorStream()); + stdout.start(); + stderr.start(); + int exitCode = process.waitFor(); + stdout.join(); + stderr.join(); + return CommandResult.completed(command, exitCode, stdout.getOutput(), stderr.getOutput()); + } +} From 2711b59b19be5c8f710b8e3f038b42c97cbc0c28 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:17:12 +0530 Subject: [PATCH 06/10] Added dedicated Root command executor --- .../command/RootCommandExecutor.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/command/RootCommandExecutor.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/RootCommandExecutor.java b/app/src/main/java/com/dhangofa/networktoggle/command/RootCommandExecutor.java new file mode 100644 index 0000000..4d53596 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/command/RootCommandExecutor.java @@ -0,0 +1,25 @@ +package com.dhangofa.networktoggle.command; + +import com.dhangofa.networktoggle.model.CommandResult; + +public final class RootCommandExecutor implements CommandExecutor { + @Override + public CommandResult execute(String command) { + Process process = null; + try { + process = Runtime.getRuntime().exec(new String[]{"su", "-c", command}); + return ProcessResultReader.collect(command, process); + } catch (Exception e) { + return CommandResult.failed(command, describe(e)); + } finally { + if (process != null) process.destroy(); + } + } + + private String describe(Exception e) { + String message = e.getMessage(); + return message == null || message.trim().isEmpty() + ? e.getClass().getSimpleName() + : e.getClass().getSimpleName() + ": " + message; + } +} From a59100f813127d5f0f1a32d8177cc88185a4ed0b Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:19:13 +0530 Subject: [PATCH 07/10] Added dedicated Shizuku command executor --- .../command/ShizukuCommandExecutor.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/command/ShizukuCommandExecutor.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/ShizukuCommandExecutor.java b/app/src/main/java/com/dhangofa/networktoggle/command/ShizukuCommandExecutor.java new file mode 100644 index 0000000..bce624a --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/command/ShizukuCommandExecutor.java @@ -0,0 +1,43 @@ +package com.dhangofa.networktoggle.command; + +import android.content.pm.PackageManager; +import com.dhangofa.networktoggle.model.CommandResult; +import java.lang.reflect.Method; +import rikka.shizuku.Shizuku; + +public final class ShizukuCommandExecutor implements CommandExecutor { + private static Method newProcessMethod; + + @Override + public CommandResult execute(String command) { + Process process = null; + try { + if (!Shizuku.pingBinder()) return CommandResult.failed(command, "Shizuku is not running."); + if (Shizuku.checkSelfPermission() != PackageManager.PERMISSION_GRANTED) { + return CommandResult.failed(command, "Shizuku permission is not granted."); + } + process = (Process) getNewProcessMethod().invoke( + null, new String[]{"sh", "-c", command}, null, null); + if (process == null) return CommandResult.failed(command, "Shizuku did not create a shell process."); + return ProcessResultReader.collect(command, process); + } catch (Exception e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + String message = cause.getMessage(); + return CommandResult.failed(command, + message == null || message.trim().isEmpty() + ? cause.getClass().getSimpleName() + : cause.getClass().getSimpleName() + ": " + message); + } finally { + if (process != null) process.destroy(); + } + } + + private static synchronized Method getNewProcessMethod() throws NoSuchMethodException { + if (newProcessMethod == null) { + newProcessMethod = Shizuku.class.getDeclaredMethod( + "newProcess", String[].class, String[].class, String.class); + newProcessMethod.setAccessible(true); + } + return newProcessMethod; + } +} From 8c63795a27338bbd97c8125ee6cfa313ccaeb17f Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:20:54 +0530 Subject: [PATCH 08/10] Prepared the command layer for detailed in-app error reporting --- .../command/StreamCollector.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/command/StreamCollector.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/StreamCollector.java b/app/src/main/java/com/dhangofa/networktoggle/command/StreamCollector.java new file mode 100644 index 0000000..543b9e4 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/command/StreamCollector.java @@ -0,0 +1,28 @@ +package com.dhangofa.networktoggle.command; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +final class StreamCollector extends Thread { + private final InputStream inputStream; + private final StringBuilder output = new StringBuilder(); + + StreamCollector(String name, InputStream inputStream) { + super(name); + this.inputStream = inputStream; + setDaemon(true); + } + + @Override + public void run() { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = reader.readLine()) != null) output.append(line).append(' +'); + } catch (Exception ignored) { + } + } + + String getOutput() { return output.toString().trim(); } +} From 266bc76e0209e9415faaa6382aa0a3a5bb31ed07 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:36:52 +0530 Subject: [PATCH 09/10] Refactor NetworkTileService to use modular command executors - Removed direct Root and Shizuku command execution from NetworkTileService. - Removed the local CommandResult implementation and legacy stream-reading methods. - Routed shell commands through CommandExecutorFactory based on the selected execution mode. - Updated command handling to use the shared CommandResult model. - Updated command result access to use exit code, stdout, and success getters. - Preserved existing network mode readback, SIM resolution, tile state, and switching behavior. --- .../networktoggle/NetworkTileService.java | 135 +++--------------- 1 file changed, 23 insertions(+), 112 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java index 72e1e50..6ee741a 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java +++ b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java @@ -1,6 +1,5 @@ package com.dhangofa.networktoggle; -import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; @@ -15,30 +14,26 @@ import android.telephony.SubscriptionManager; import android.widget.Toast; +import com.dhangofa.networktoggle.command.CommandExecutor; +import com.dhangofa.networktoggle.command.CommandExecutorFactory; import com.dhangofa.networktoggle.config.AppPreferences; +import com.dhangofa.networktoggle.model.CommandResult; import com.dhangofa.networktoggle.model.ExecutionMode; import com.dhangofa.networktoggle.model.NetworkMode; import com.dhangofa.networktoggle.model.TargetSim; -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.lang.reflect.Method; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import java.util.regex.Pattern; -import rikka.shizuku.Shizuku; - public class NetworkTileService extends TileService { private static final int INVALID_SLOT_INDEX = -1; private static final int INVALID_SUB_ID = -1; 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 static Method shizukuNewProcessMethod; private static Icon icon4g; private static Icon icon5g; @@ -115,24 +110,6 @@ public void onClick() { }); } - private static Method getShizukuNewProcessMethod() throws NoSuchMethodException { - if (shizukuNewProcessMethod == null) { - shizukuNewProcessMethod = Shizuku.class.getDeclaredMethod( - "newProcess", String[].class, String[].class, String.class); - shizukuNewProcessMethod.setAccessible(true); - } - return shizukuNewProcessMethod; - } - - 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); @@ -217,57 +194,17 @@ private NetworkMode readCurrentNetworkMode() { } CommandResult result = runCommandForResult(executionMode, command); - if (result.exitCode != 0) return NetworkMode.UNKNOWN; - return NetworkMode.fromLegacyMode(extractFirstInt(result.stdout)); - } - - private CommandResult runCommandForResult(ExecutionMode mode, String command) { - if (mode == ExecutionMode.SHIZUKU) return runCommandForResultWithShizuku(command); - if (mode == ExecutionMode.ROOT) return runCommandForResultWithRoot(command); - return new CommandResult(-1, ""); - } - - private CommandResult runCommandForResultWithRoot(String command) { - Process process = null; - try { - process = Runtime.getRuntime().exec(new String[]{"su", "-c", command}); - int exitCode = process.waitFor(); - return new CommandResult(exitCode, readStream(process.getInputStream())); - } catch (Exception e) { - return new CommandResult(-1, ""); - } finally { - if (process != null) process.destroy(); - } - } - - private CommandResult runCommandForResultWithShizuku(String command) { - Process process = null; - try { - if (!Shizuku.pingBinder() - || Shizuku.checkSelfPermission() != 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(); - return new CommandResult(exitCode, readStream(process.getInputStream())); - } catch (Exception e) { - return new CommandResult(-1, ""); - } finally { - if (process != null) process.destroy(); - } - } - - private String readStream(InputStream stream) { - StringBuilder builder = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { - String line; - while ((line = reader.readLine()) != null) builder.append(line).append('\n'); - } catch (Exception ignored) { - } - return builder.toString().trim(); + if (result.getExitCode() != 0) return NetworkMode.UNKNOWN; + return NetworkMode.fromLegacyMode(extractFirstInt(result.getStdout())); } + + private CommandResult runCommandForResult(ExecutionMode mode, String command) { + CommandExecutor executor = CommandExecutorFactory.forMode(mode); + if (executor == null) { + return CommandResult.failed(command, "No execution mode selected."); + } + return executor.execute(command); + } private Integer extractFirstInt(String text) { try { @@ -332,7 +269,7 @@ private int resolveSlotIndexFromDumpsys(ExecutionMode executionMode, int dataSub + "([^0-9]| )\" | head -n 1 | grep -o -E \"simSlotIndex=[0-9]+\" " + "| cut -d '=' -f 2"; CommandResult result = runCommandForResult(executionMode, command); - Integer slot = result.exitCode == 0 ? extractFirstInt(result.stdout) : null; + Integer slot = result.getExitCode() == 0 ? extractFirstInt(result.getStdout()) : null; return slot != null && isValidSlotIndex(slot) ? slot : INVALID_SLOT_INDEX; } @@ -341,44 +278,18 @@ private int resolveSubIdFromDumpsys(ExecutionMode executionMode, int slotIndex) + "([^0-9]| )\" | head -n 1 | grep -o -E \"\\{id=[0-9]+\" " + "| cut -d '=' -f 2"; CommandResult result = runCommandForResult(executionMode, command); - Integer subId = result.exitCode == 0 ? extractFirstInt(result.stdout) : null; + Integer subId = result.getExitCode() == 0 ? extractFirstInt(result.getStdout()) : null; return subId != null && subId > 0 ? subId : INVALID_SUB_ID; } - private boolean applyNetworkMode(NetworkMode mode, ExecutionMode executionMode) { - int slotIndex = resolveTargetSlotIndex(executionMode); - if (!isValidSlotIndex(slotIndex) || mode.getBinaryMask() == null) return false; - String command = "cmd phone set-allowed-network-types-for-users -s " - + slotIndex + " " + mode.getBinaryMask(); - if (executionMode == ExecutionMode.SHIZUKU) return runCommandWithShizuku(command); - if (executionMode == ExecutionMode.ROOT) return runCommandWithRoot(command); - return false; - } + private boolean applyNetworkMode(NetworkMode mode, ExecutionMode executionMode) { + int slotIndex = resolveTargetSlotIndex(executionMode); + if (!isValidSlotIndex(slotIndex) || mode.getBinaryMask() == null) return false; - private boolean runCommandWithRoot(String command) { - Process process = null; - try { - process = Runtime.getRuntime().exec(new String[]{"su", "-c", command}); - return process.waitFor() == 0; - } catch (Exception e) { - return false; - } finally { - if (process != null) process.destroy(); - } - } + String command = "cmd phone set-allowed-network-types-for-users -s " + + slotIndex + " " + mode.getBinaryMask(); + CommandResult result = runCommandForResult(executionMode, command); + return result.isSuccess(); + } - private boolean runCommandWithShizuku(String command) { - Process process = null; - try { - if (!Shizuku.pingBinder() - || Shizuku.checkSelfPermission() != PackageManager.PERMISSION_GRANTED) return false; - process = (Process) getShizukuNewProcessMethod().invoke( - null, new String[]{"sh", "-c", command}, null, null); - return process != null && process.waitFor() == 0; - } catch (Exception e) { - return false; - } finally { - if (process != null) process.destroy(); - } - } } From 4b701f97bcd1a952c872bdc52b48cdadbea16836 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:45:00 +0530 Subject: [PATCH 10/10] Fix newline character in StreamCollector - Corrected the broken newline character literal in StreamCollector. - Restored valid Java syntax for appending captured process output. - Fixes the Phase 2 release compilation failure. --- .../dhangofa/networktoggle/command/StreamCollector.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/StreamCollector.java b/app/src/main/java/com/dhangofa/networktoggle/command/StreamCollector.java index 543b9e4..efaca66 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/command/StreamCollector.java +++ b/app/src/main/java/com/dhangofa/networktoggle/command/StreamCollector.java @@ -18,11 +18,14 @@ final class StreamCollector extends Thread { public void run() { try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { String line; - while ((line = reader.readLine()) != null) output.append(line).append(' -'); + while ((line = reader.readLine()) != null) { + output.append(line).append('\n'); + } } catch (Exception ignored) { } } - String getOutput() { return output.toString().trim(); } + String getOutput() { + return output.toString().trim(); + } }