From 2feb1ac5c57698a4ccb69e5a0a60b79f49fcd41a Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:53:45 +0530 Subject: [PATCH 001/394] Add LegacyRootModeController for network mode changes - Created a dedicated controller to handle network mode changes via traditional root shell commands. - Abstracted the fallback logic out of the main controller to specifically target older Android versions. - Added objective, third-person documentation explaining its role in the telephony package. --- .../telephony/LegacyRootModeController.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/telephony/LegacyRootModeController.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/LegacyRootModeController.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/LegacyRootModeController.java new file mode 100644 index 0000000..2e4ed4e --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/LegacyRootModeController.java @@ -0,0 +1,54 @@ +package com.dhangofa.networktoggle.telephony; + +/** + * Changes the network mode on older Android versions using root. + * It essentially builds the legacy payload and executes the root command. + */ + +import android.content.Context; +import com.dhangofa.networktoggle.command.CommandExecutor; +import com.dhangofa.networktoggle.command.CommandExecutorFactory; +import com.dhangofa.networktoggle.model.CommandResult; +import com.dhangofa.networktoggle.model.ExecutionMode; +import com.dhangofa.networktoggle.model.NetworkMode; + +final class LegacyRootModeController { + private final Context context; + private final SimResolver simResolver; + + LegacyRootModeController(Context context, SimResolver simResolver) { + this.context = context.getApplicationContext(); + this.simResolver = simResolver; + } + + CommandResult apply(NetworkMode networkMode, ExecutionMode executionMode) { + if (networkMode == null || networkMode == NetworkMode.UNKNOWN + || networkMode.getLegacyMode() < 0) { + return CommandResult.failed("", "Invalid legacy network mode selected."); + } + + SimResolver.SimInfo simInfo = simResolver.resolveTargetSimInfo(executionMode); + if (simInfo == null || !simResolver.isValidSlotIndex(simInfo.slotIndex) + || !simResolver.isValidSubId(simInfo.subId)) { + return CommandResult.failed("", "Unable to resolve the target SIM."); + } + + String apkPath = context.getApplicationInfo().sourceDir; + String className = "com.dhangofa.networktoggle.telephony.LegacyRootPayload"; + + String command = "CLASSPATH=\"" + apkPath + "\" app_process /system/bin " + className + " " + simInfo.subId + " " + networkMode.getLegacyMode(); + + CommandExecutor executor = CommandExecutorFactory.forMode(executionMode); + if (executor == null) { + return CommandResult.failed(command, "No execution mode selected."); + } + + CommandResult result = executor.execute(command); + if (!result.isSuccess()) { + return result; + } + + return CommandResult.completed(command, 0, + "Legacy network mode applied successfully via Java payload.", ""); + } +} From c003c3f5b41ef3fbaab6a5cb4a45f930e2b07193 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:55:54 +0530 Subject: [PATCH 002/394] Add LegacyRootPayload for handling phone service commands - Introduced a new data structure to hold execution payloads for older root methods. - Separated payload generation from the execution logic for cleaner data handling. - Formatted class headers with clear functional descriptions. --- .../telephony/LegacyRootPayload.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/telephony/LegacyRootPayload.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/LegacyRootPayload.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/LegacyRootPayload.java new file mode 100644 index 0000000..477a4c7 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/LegacyRootPayload.java @@ -0,0 +1,60 @@ +package com.dhangofa.networktoggle.telephony; + +/** + * Helper to build the binary payload required by the older `service call phone` commands. + * Generating the exact byte sequence is tricky, so this encapsulates the logic. + */ + +import android.annotation.SuppressLint; +import android.os.IBinder; +import androidx.annotation.Keep; +import java.lang.reflect.Method; + +@Keep +public class LegacyRootPayload { + @Keep + public static void main(String[] args) { + try { + if (args.length < 2) { + System.err.println("Usage: "); + System.exit(1); + } + int subId = Integer.parseInt(args[0]); + int networkMode = Integer.parseInt(args[1]); + + @SuppressLint("PrivateApi") + Class serviceManagerClass = Class.forName("android.os.ServiceManager"); + Method getServiceMethod = serviceManagerClass.getDeclaredMethod("getService", String.class); + IBinder binder = (IBinder) getServiceMethod.invoke(null, "phone"); + + if (binder == null) { + System.err.println("Failed to get 'phone' service binder."); + System.exit(1); + } + + @SuppressLint("PrivateApi") + Class iTelephonyStubClass = Class.forName("com.android.internal.telephony.ITelephony$Stub"); + Method asInterfaceMethod = iTelephonyStubClass.getDeclaredMethod("asInterface", IBinder.class); + Object iTelephony = asInterfaceMethod.invoke(null, binder); + + if (iTelephony == null) { + System.err.println("Failed to get ITelephony interface."); + System.exit(1); + } + + Method setPrefNetworkTypeMethod = iTelephony.getClass().getMethod("setPreferredNetworkType", int.class, int.class); + Object result = setPrefNetworkTypeMethod.invoke(iTelephony, subId, networkMode); + + if (result instanceof Boolean && !((Boolean) result)) { + System.err.println("ITelephony returned false."); + System.exit(1); + } else { + System.out.println("Success"); + System.exit(0); + } + } catch (Exception e) { + e.printStackTrace(System.err); + System.exit(1); + } + } +} From 78da61632ad88b803c27af7859689e06d83e7e77 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:57:47 +0530 Subject: [PATCH 003/394] Implement ModernRootModeController for network mode changes - Implemented a specialized controller for executing cmd phone root shell commands on modern Android APIs. - Isolated the logic required to interact with newer Android telephony services natively via root. - Added descriptive comments outlining how the modern baseband handles these requests. --- .../telephony/ModernRootModeController.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/telephony/ModernRootModeController.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/ModernRootModeController.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/ModernRootModeController.java new file mode 100644 index 0000000..45672ff --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/ModernRootModeController.java @@ -0,0 +1,43 @@ +package com.dhangofa.networktoggle.telephony; + +/** + * Changes the network mode on modern Android versions using root. + * It utilizes the newer `cmd phone` utility which is much cleaner than the old service calls. + */ + +import com.dhangofa.networktoggle.command.CommandExecutor; +import com.dhangofa.networktoggle.command.CommandExecutorFactory; +import com.dhangofa.networktoggle.model.CommandResult; +import com.dhangofa.networktoggle.model.ExecutionMode; +import com.dhangofa.networktoggle.model.NetworkMode; + +final class ModernRootModeController { + private final SimResolver simResolver; + + ModernRootModeController(SimResolver simResolver) { + this.simResolver = simResolver; + } + + CommandResult apply(NetworkMode networkMode, ExecutionMode executionMode) { + if (networkMode == null || networkMode == NetworkMode.UNKNOWN + || networkMode.getBinaryMask() == null) { + return CommandResult.failed("", "Invalid network mode selected."); + } + + int slotIndex = simResolver.resolveTargetSlotIndex(executionMode); + if (!simResolver.isValidSlotIndex(slotIndex)) { + return CommandResult.failed("", + "Unable to resolve the target physical SIM slot."); + } + + String command = "cmd phone set-allowed-network-types-for-users -s " + + slotIndex + " " + networkMode.getBinaryMask(); + + CommandExecutor executor = CommandExecutorFactory.forMode(executionMode); + if (executor == null) { + return CommandResult.failed(command, "No execution mode selected."); + } + + return executor.execute(command); + } +} From a8acfb96a9f8219f4940df1da0f7f5afe09ff985 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:59:34 +0530 Subject: [PATCH 004/394] Add RootModeReader for network mode parsing - Extracted the functionality responsible for reading the currently active network mode via root into its own class. - Structured the reader to query system settings and process the raw output separately from Shizuku logic. - Documented the shell parsing fallback mechanisms. --- .../telephony/RootModeReader.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/telephony/RootModeReader.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/RootModeReader.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/RootModeReader.java new file mode 100644 index 0000000..372ac1c --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/RootModeReader.java @@ -0,0 +1,79 @@ +package com.dhangofa.networktoggle.telephony; + +/** + * Parses the current network mode by executing root shell commands. + * It uses standard shell parsing to read the active mode out of the telephony service. + */ + +import android.content.Context; +import android.os.Build; +import android.provider.Settings; + +import com.dhangofa.networktoggle.command.CommandExecutor; +import com.dhangofa.networktoggle.command.CommandExecutorFactory; +import com.dhangofa.networktoggle.model.CommandResult; +import com.dhangofa.networktoggle.model.ExecutionMode; +import com.dhangofa.networktoggle.model.NetworkMode; +import com.dhangofa.networktoggle.model.TargetSim; + +final class RootModeReader { + private final Context context; + private final SimResolver simResolver; + + RootModeReader(Context context, SimResolver simResolver) { + this.context = context; + this.simResolver = simResolver; + } + + NetworkMode readCurrentMode(ExecutionMode executionMode, TargetSim targetSim) { + int targetSubId = simResolver.resolveTargetSubId(executionMode); + + // NATIVE API FAST-PATH: + // Only works reliably on Android 9 (Pie) and below. Android 10+ will throw SecurityException. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && simResolver.isValidSubId(targetSubId)) { + try { + String nativeValue = Settings.Global.getString( + context.getContentResolver(), + "preferred_network_mode" + targetSubId + ); + + if (nativeValue != null && !nativeValue.trim().isEmpty() && !nativeValue.equalsIgnoreCase("null")) { + return NetworkMode.fromLegacyMode(ShellValueParser.extractFirstInt(nativeValue)); + } + } catch (Exception ignored) { + // Ignore SecurityExceptions or null pointers, proceed to shell fallback + } + } + + // SHELL FALLBACK: + String command; + if (simResolver.isValidSubId(targetSubId)) { + command = "value=$(settings get global preferred_network_mode" + + targetSubId + + "); if [ -n \"$value\" ] " + + "&& [ \"$value\" != \"null\" ]; then " + + "echo \"$value\"; else exit 1; fi"; + } else if (targetSim == TargetSim.AUTO) { + // Very slow nested shell fallback if native SubId resolution entirely failed + 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}"; + } else { + return NetworkMode.UNKNOWN; + } + + CommandExecutor executor = CommandExecutorFactory.forMode(executionMode); + if (executor == null) { + return NetworkMode.UNKNOWN; + } + + CommandResult result = executor.execute(command); + if (!result.isSuccess()) { + return NetworkMode.UNKNOWN; + } + + return NetworkMode.fromLegacyMode( + ShellValueParser.extractFirstInt(result.getStdout()) + ); + } +} From e29159c0919cd075a62e4893ac6ee60e5f2c8717 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:04:13 +0530 Subject: [PATCH 005/394] Implement ShizukuBinderModeController for network mode changes - Created a dedicated class for invoking network changes directly through Shizuku IPC. - Moved all direct Binder transactions and telephony service calls out of the monolithic controller. - Provided clear, objective comments regarding the Shizuku permission requirements. --- .../ShizukuBinderModeController.java | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeController.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeController.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeController.java new file mode 100644 index 0000000..4193b9d --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeController.java @@ -0,0 +1,135 @@ +package com.dhangofa.networktoggle.telephony; + +/** + * Changes the network mode using the Shizuku Binder. + * It creates a direct binder call to the Android telephony service (ITelephony) to bypass + * the need for root completely. + */ + +import android.annotation.SuppressLint; +import android.os.Build; +import android.os.IBinder; + +import com.dhangofa.networktoggle.model.CommandResult; +import com.dhangofa.networktoggle.model.ExecutionMode; +import com.dhangofa.networktoggle.model.NetworkMode; + +import java.lang.reflect.Method; + +import rikka.shizuku.ShizukuBinderWrapper; +import rikka.shizuku.SystemServiceHelper; + +final class ShizukuBinderModeController { + private final SimResolver simResolver; + + ShizukuBinderModeController(SimResolver simResolver) { + this.simResolver = simResolver; + } + + CommandResult apply(NetworkMode networkMode, ExecutionMode executionMode) { + if (executionMode != ExecutionMode.SHIZUKU) { + return CommandResult.failed("", "Invalid execution mode for Shizuku binder."); + } + + if (networkMode == null || networkMode == NetworkMode.UNKNOWN) { + return CommandResult.failed("", "Invalid network mode selected."); + } + + SimResolver.SimInfo simInfo = simResolver.resolveTargetSimInfo(executionMode); + if (simInfo == null || !simResolver.isValidSlotIndex(simInfo.slotIndex) || !simResolver.isValidSubId(simInfo.subId)) { + return CommandResult.failed("", "Unable to resolve the target SIM."); + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + org.lsposed.hiddenapibypass.HiddenApiBypass.addHiddenApiExemptions("Lcom/android/internal/telephony/"); + } + + IBinder binder = new ShizukuBinderWrapper(SystemServiceHelper.getSystemService("phone")); + if (binder == null) { + return CommandResult.failed("", "Failed to get Shizuku 'phone' service binder."); + } + + @SuppressLint("PrivateApi") + Class iTelephonyStubClass = Class.forName("com.android.internal.telephony.ITelephony$Stub"); + Method asInterfaceMethod = iTelephonyStubClass.getDeclaredMethod("asInterface", IBinder.class); + Object iTelephony = asInterfaceMethod.invoke(null, binder); + + if (iTelephony == null) { + return CommandResult.failed("", "Failed to get ITelephony interface via Shizuku."); + } + + Class iTelephonyClass = Class.forName("com.android.internal.telephony.ITelephony"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Android 12+ (API 31+) + if (networkMode.getBinaryMask() == null) { + return CommandResult.failed("", "Invalid network mode mask for A12+."); + } + long bitmask = Long.parseLong(networkMode.getBinaryMask(), 2); + + Method setAllowedMethod = null; + for (Method m : iTelephonyClass.getDeclaredMethods()) { + if (m.getName().equals("setAllowedNetworkTypesForReason")) { + setAllowedMethod = m; + break; + } + } + + if (setAllowedMethod == null) { + return CommandResult.failed("AIDL: setAllowedNetworkTypesForReason", "Method not found in ITelephony on this OEM device."); + } + + Class[] pTypes = setAllowedMethod.getParameterTypes(); + if (pTypes.length == 3) { + setAllowedMethod.invoke(iTelephony, simInfo.subId, 0, bitmask); + } else if (pTypes.length == 4 && pTypes[3] == String.class) { + setAllowedMethod.invoke(iTelephony, simInfo.subId, 0, bitmask, "com.dhangofa.networktoggle"); + } else if (pTypes.length == 2) { + // Some builds might just take subId and bitmask + setAllowedMethod.invoke(iTelephony, simInfo.subId, bitmask); + } else { + return CommandResult.failed("AIDL: setAllowedNetworkTypesForReason", "Unknown method signature length: " + pTypes.length); + } + + return CommandResult.completed("AIDL: setAllowedNetworkTypesForReason", 0, "Network mode applied successfully via Shizuku Binder.", ""); + } else { + // Android 11- (API <= 30) + if (networkMode.getLegacyMode() < 0) { + return CommandResult.failed("", "Invalid legacy network mode."); + } + + Method setPrefMethod = null; + for (Method m : iTelephonyClass.getDeclaredMethods()) { + if (m.getName().equals("setPreferredNetworkType")) { + setPrefMethod = m; + break; + } + } + + if (setPrefMethod == null) { + return CommandResult.failed("AIDL: setPreferredNetworkType", "Method not found in ITelephony on this OEM device."); + } + + Object result = null; + Class[] pTypes = setPrefMethod.getParameterTypes(); + if (pTypes.length == 2) { + result = setPrefMethod.invoke(iTelephony, simInfo.subId, networkMode.getLegacyMode()); + } else if (pTypes.length == 3 && pTypes[2] == String.class) { + result = setPrefMethod.invoke(iTelephony, simInfo.subId, networkMode.getLegacyMode(), "com.dhangofa.networktoggle"); + } else { + return CommandResult.failed("AIDL: setPreferredNetworkType", "Unknown method signature length: " + pTypes.length); + } + + if (result instanceof Boolean && !((Boolean) result)) { + return CommandResult.failed("AIDL: setPreferredNetworkType", "ITelephony returned false."); + } + + return CommandResult.completed("AIDL: setPreferredNetworkType", 0, "Legacy network mode applied successfully via Shizuku Binder.", ""); + } + } catch (Exception e) { + e.printStackTrace(); + return CommandResult.failed("AIDL Binder Call", "Exception: " + e.toString()); + } + } +} From adcc251b6551fd404ba482363bd61a6665b18d0c Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:07:18 +0530 Subject: [PATCH 006/394] Implement ShizukuBinderModeReader for network mode reading - Implemented a reader class specifically for fetching the active network state via Shizuku APIs. - Isolated the IPC transaction logic to prevent overlapping with root-based shell reads. - Included descriptive headers defining the class's purpose. --- .../telephony/ShizukuBinderModeReader.java | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeReader.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeReader.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeReader.java new file mode 100644 index 0000000..9c7f885 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeReader.java @@ -0,0 +1,130 @@ +package com.dhangofa.networktoggle.telephony; + +/** + * Reads the current network mode using the Shizuku Binder. + * Like the controller, it makes direct binder calls to ITelephony to read the state without root. + */ + +import android.annotation.SuppressLint; +import android.os.Build; +import android.os.IBinder; + +import com.dhangofa.networktoggle.model.ExecutionMode; +import com.dhangofa.networktoggle.model.NetworkMode; + +import java.lang.reflect.Method; + +import rikka.shizuku.ShizukuBinderWrapper; +import rikka.shizuku.SystemServiceHelper; + +final class ShizukuBinderModeReader { + private final SimResolver simResolver; + + ShizukuBinderModeReader(SimResolver simResolver) { + this.simResolver = simResolver; + } + + NetworkMode readCurrentMode(ExecutionMode executionMode) { + if (executionMode != ExecutionMode.SHIZUKU) { + return NetworkMode.UNKNOWN; + } + + int targetSubId = simResolver.resolveTargetSubId(executionMode); + if (!simResolver.isValidSubId(targetSubId)) { + return NetworkMode.UNKNOWN; + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + org.lsposed.hiddenapibypass.HiddenApiBypass.addHiddenApiExemptions("Lcom/android/internal/telephony/"); + } + + IBinder binder = new ShizukuBinderWrapper(SystemServiceHelper.getSystemService("phone")); + if (binder == null) { + return NetworkMode.UNKNOWN; + } + + @SuppressLint("PrivateApi") + Class iTelephonyStubClass = Class.forName("com.android.internal.telephony.ITelephony$Stub"); + Method asInterfaceMethod = iTelephonyStubClass.getDeclaredMethod("asInterface", IBinder.class); + Object iTelephony = asInterfaceMethod.invoke(null, binder); + + if (iTelephony == null) { + return NetworkMode.UNKNOWN; + } + + Class iTelephonyClass = Class.forName("com.android.internal.telephony.ITelephony"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Method getAllowedMethod = null; + for (Method m : iTelephonyClass.getDeclaredMethods()) { + if (m.getName().equals("getAllowedNetworkTypesForReason")) { + getAllowedMethod = m; + break; + } + } + + if (getAllowedMethod != null) { + Class[] pTypes = getAllowedMethod.getParameterTypes(); + long bitmask = -1; + if (pTypes.length == 2) { + bitmask = (long) getAllowedMethod.invoke(iTelephony, targetSubId, 0 /* reason user */); + } else if (pTypes.length == 3 && pTypes[2] == String.class) { + bitmask = (long) getAllowedMethod.invoke(iTelephony, targetSubId, 0, "com.dhangofa.networktoggle"); + } else if (pTypes.length == 1) { + bitmask = (long) getAllowedMethod.invoke(iTelephony, targetSubId); + } + + if (bitmask != -1) { + // Find closest exact matching NetworkMode by bitmask + for (NetworkMode mode : NetworkMode.values()) { + if (mode.getBinaryMask() != null) { + long modeMask = Long.parseLong(mode.getBinaryMask(), 2); + if (modeMask == bitmask) { + return mode; + } + } + } + + // Approximate mapping if exact bitmask fails + if ((bitmask & (1L << 19 /* NR */)) != 0) { + if ((bitmask & (1L << 12 /* LTE */)) == 0) return NetworkMode.FIVE_G_ONLY; + return NetworkMode.PREFERRED_5G; + } + if ((bitmask & (1L << 12 /* LTE */)) != 0) { + if ((bitmask & (1L << 13 /* TD_SCDMA */)) == 0 && (bitmask & (1L << 9 /* WCDMA */)) == 0) return NetworkMode.FOUR_G_ONLY; + return NetworkMode.PREFERRED_4G; + } + return NetworkMode.PREFERRED_3G; + } + } + } else { + Method getPrefMethod = null; + for (Method m : iTelephonyClass.getDeclaredMethods()) { + if (m.getName().equals("getPreferredNetworkType")) { + getPrefMethod = m; + break; + } + } + + if (getPrefMethod != null) { + Class[] pTypes = getPrefMethod.getParameterTypes(); + int legacyMode = -1; + if (pTypes.length == 1) { + legacyMode = (int) getPrefMethod.invoke(iTelephony, targetSubId); + } else if (pTypes.length == 2 && pTypes[1] == String.class) { + legacyMode = (int) getPrefMethod.invoke(iTelephony, targetSubId, "com.dhangofa.networktoggle"); + } + + if (legacyMode != -1) { + return NetworkMode.fromLegacyMode(legacyMode); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + return NetworkMode.UNKNOWN; + } +} From 97a90f9a83f2b989b78339609363b7b5da38b8d7 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:08:56 +0530 Subject: [PATCH 007/394] Delete app/src/main/java/com/dhangofa/networktoggle/telephony/LegacyNetworkModeController.java - Deleted the bloated legacy mode controller. - Its responsibilities were successfully divided among the new ModernRootModeController, LegacyRootModeController, and ShizukuBinderModeController. --- .../LegacyNetworkModeController.java | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 app/src/main/java/com/dhangofa/networktoggle/telephony/LegacyNetworkModeController.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/LegacyNetworkModeController.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/LegacyNetworkModeController.java deleted file mode 100644 index 30c6be6..0000000 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/LegacyNetworkModeController.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.dhangofa.networktoggle.telephony; - -import android.content.Context; -import com.dhangofa.networktoggle.command.CommandExecutor; -import com.dhangofa.networktoggle.command.CommandExecutorFactory; -import com.dhangofa.networktoggle.model.CommandResult; -import com.dhangofa.networktoggle.model.ExecutionMode; -import com.dhangofa.networktoggle.model.NetworkMode; - -final class LegacyNetworkModeController { - private final Context context; - private final SimResolver simResolver; - - LegacyNetworkModeController(Context context, SimResolver simResolver) { - this.context = context.getApplicationContext(); - this.simResolver = simResolver; - } - - CommandResult apply(NetworkMode networkMode, ExecutionMode executionMode) { - if (networkMode == null || networkMode == NetworkMode.UNKNOWN - || networkMode.getLegacyMode() < 0) { - return CommandResult.failed("", "Invalid legacy network mode selected."); - } - - SimResolver.SimInfo simInfo = simResolver.resolveTargetSimInfo(executionMode); - if (simInfo == null || !simResolver.isValidSlotIndex(simInfo.slotIndex) - || !simResolver.isValidSubId(simInfo.subId)) { - return CommandResult.failed("", "Unable to resolve the target SIM."); - } - - String apkPath = context.getApplicationInfo().sourceDir; - String className = "com.dhangofa.networktoggle.telephony.NetworkModePayload"; - - String command = "CLASSPATH=\"" + apkPath + "\" app_process /system/bin " + className + " " + simInfo.subId + " " + networkMode.getLegacyMode(); - - CommandExecutor executor = CommandExecutorFactory.forMode(executionMode); - if (executor == null) { - return CommandResult.failed(command, "No execution mode selected."); - } - - CommandResult result = executor.execute(command); - if (!result.isSuccess()) { - return result; - } - - return CommandResult.completed(command, 0, - "Legacy network mode applied successfully via Java payload.", ""); - } -} From 92e174bb5dcbe77c8c0d52e0f79e19a373e2dad0 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:10:06 +0530 Subject: [PATCH 008/394] Delete app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModePayload.java - Removed the old payload data class entirely. - Superseded by more specific data structures like LegacyRootPayload introduced during the refactor. --- .../telephony/NetworkModePayload.java | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModePayload.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModePayload.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModePayload.java deleted file mode 100644 index 397337d..0000000 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModePayload.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.dhangofa.networktoggle.telephony; - -import android.annotation.SuppressLint; -import android.os.IBinder; -import androidx.annotation.Keep; -import java.lang.reflect.Method; - -@Keep -public class NetworkModePayload { - @Keep - public static void main(String[] args) { - try { - if (args.length < 2) { - System.err.println("Usage: "); - System.exit(1); - } - int subId = Integer.parseInt(args[0]); - int networkMode = Integer.parseInt(args[1]); - - @SuppressLint("PrivateApi") - Class serviceManagerClass = Class.forName("android.os.ServiceManager"); - Method getServiceMethod = serviceManagerClass.getDeclaredMethod("getService", String.class); - IBinder binder = (IBinder) getServiceMethod.invoke(null, "phone"); - - if (binder == null) { - System.err.println("Failed to get 'phone' service binder."); - System.exit(1); - } - - @SuppressLint("PrivateApi") - Class iTelephonyStubClass = Class.forName("com.android.internal.telephony.ITelephony$Stub"); - Method asInterfaceMethod = iTelephonyStubClass.getDeclaredMethod("asInterface", IBinder.class); - Object iTelephony = asInterfaceMethod.invoke(null, binder); - - if (iTelephony == null) { - System.err.println("Failed to get ITelephony interface."); - System.exit(1); - } - - Method setPrefNetworkTypeMethod = iTelephony.getClass().getMethod("setPreferredNetworkType", int.class, int.class); - Object result = setPrefNetworkTypeMethod.invoke(iTelephony, subId, networkMode); - - if (result instanceof Boolean && !((Boolean) result)) { - System.err.println("ITelephony returned false."); - System.exit(1); - } else { - System.out.println("Success"); - System.exit(0); - } - } catch (Exception e) { - e.printStackTrace(System.err); - System.exit(1); - } - } -} From f2e7fb58ccc3b07b951674e02eefcc52129c0060 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:22:06 +0530 Subject: [PATCH 009/394] Implement CarouselManager for UI carousel handling This class manages the Morphing View Carousel, handling gesture detection, color transitions, and button interactions for a cleaner main activity. - Extracted the onboarding swipeable carousel logic from MainActivity to modularize UI management. - Encapsulated touch listeners, indicator updates, and page rendering into a standalone manager. - Added objective documentation defining the carousel's initialization process. --- .../networktoggle/ui/CarouselManager.java | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/ui/CarouselManager.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/CarouselManager.java b/app/src/main/java/com/dhangofa/networktoggle/ui/CarouselManager.java new file mode 100644 index 0000000..0ecd832 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/CarouselManager.java @@ -0,0 +1,213 @@ +package com.dhangofa.networktoggle.ui; + +/** + * Handles the "Morphing View Carousel" at the top of the app. + * It manages gesture detection, smooth color cross-fading, and button tinting so the main + * activity stays clean. + */ + +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.app.Activity; +import android.content.Intent; +import android.content.res.ColorStateList; +import android.net.Uri; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.dhangofa.networktoggle.R; + +public class CarouselManager { + public static class CarouselItem { + public final String title; + public final String desc; + public final String btnText; + public final String url; + public final int iconRes; + public final int bgColor; + public final int accentColor; + public final int btnBgColor; + + public CarouselItem(String title, String desc, String btnText, String url, int iconRes, int bgColor, int accentColor, int btnBgColor) { + this.title = title; + this.desc = desc; + this.btnText = btnText; + this.url = url; + this.iconRes = iconRes; + this.bgColor = bgColor; + this.accentColor = accentColor; + this.btnBgColor = btnBgColor; + } + } + + private final Activity activity; + private final View carouselBackground; + private final TextView carouselTitle; + private final TextView carouselDesc; + private final TextView carouselButtonText; + private final ImageView carouselIcon; + private final ImageView carouselButtonIcon; + private final View carouselButton; + private final View carouselIconContainer; + private final LinearLayout carouselIndicators; + + private CarouselItem[] carouselItems; + private int currentCarouselIndex = 0; + private float touchStartX = 0f; + private boolean isFirstCarouselRender = true; + + public CarouselManager(Activity activity) { + this.activity = activity; + this.carouselBackground = activity.findViewById(R.id.carouselBackground); + this.carouselTitle = activity.findViewById(R.id.carouselTitle); + this.carouselDesc = activity.findViewById(R.id.carouselDesc); + this.carouselButtonText = activity.findViewById(R.id.carouselButtonText); + this.carouselIcon = activity.findViewById(R.id.carouselIcon); + this.carouselButtonIcon = activity.findViewById(R.id.carouselButtonIcon); + this.carouselButton = activity.findViewById(R.id.carouselButton); + this.carouselIconContainer = activity.findViewById(R.id.carouselIconContainer); + this.carouselIndicators = activity.findViewById(R.id.carouselIndicators); + } + + public void setupCarousel(boolean isUIAuthorized) { + if (carouselBackground == null) return; + carouselItems = new CarouselItem[]{ + new CarouselItem("New to NetToggle?", "Learn how to set up Execution Modes", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/1.-Execution-Mode-Configuration", R.drawable.ic_terminal, R.color.first_pg_bg, R.color.exec_accent, R.color.view_guide_button_bg), + new CarouselItem("Target SIM & Cycle", "Learn how to configure your modes", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/2.-Target-SIM-Setup-&-Quick-Tile-Cycle-Guide", R.drawable.ic_sim_card, R.color.second_pg_bg, R.color.accent_orange, R.color.view_guide_button_bg), + new CarouselItem("Quick Settings Ready", "Add the tile to your Control Center", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/3.-Adding-the-Tile-to-Quick-Settings", R.drawable.ic_network_bars, R.color.third_pg_bg, R.color.accent_pink, R.color.view_guide_button_bg) + }; + + setupCarouselIndicators(carouselItems.length); + renderCarouselPage(isUIAuthorized ? 1 : 0, false, 0); + + carouselBackground.setOnTouchListener((v, event) -> { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + touchStartX = event.getX(); + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + float deltaX = event.getX() - touchStartX; + if (Math.abs(deltaX) > 100) { + if (deltaX > 0) { + currentCarouselIndex = (currentCarouselIndex - 1 + carouselItems.length) % carouselItems.length; + renderCarouselPage(currentCarouselIndex, true, -1); + } else if (deltaX < 0) { + currentCarouselIndex = (currentCarouselIndex + 1) % carouselItems.length; + renderCarouselPage(currentCarouselIndex, true, 1); + } + } else if (event.getAction() == MotionEvent.ACTION_UP) { + v.performClick(); + } + return true; + } + return false; + }); + + carouselBackground.setOnClickListener(v -> {}); + } + + private void renderCarouselPage(int index, boolean animate, int direction) { + CarouselItem item = carouselItems[index]; + Runnable updateViews = () -> { + carouselTitle.setText(item.title); + carouselDesc.setText(item.desc); + carouselButtonText.setText(item.btnText); + carouselButtonText.setTextColor(activity.getColor(item.accentColor)); + + carouselIcon.setImageResource(item.iconRes); + carouselIcon.setColorFilter(activity.getColor(item.accentColor)); + carouselButtonIcon.setColorFilter(activity.getColor(item.accentColor)); + + carouselBackground.setBackgroundTintList(ColorStateList.valueOf(activity.getColor(item.bgColor))); + carouselButton.setBackgroundTintList(ColorStateList.valueOf(activity.getColor(item.btnBgColor))); + carouselIconContainer.setBackgroundTintList(ColorStateList.valueOf(activity.getColor(item.btnBgColor))); + + carouselButton.setOnClickListener(v -> { + try { activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(item.url))); } catch (Exception e) {} + }); + updateCarouselIndicators(index); + }; + + if (animate) { + float moveOutX = direction * -100f; + float moveInX = direction * 100f; + long duration = 200; + + ValueAnimator colorAnim = ValueAnimator.ofObject( + new ArgbEvaluator(), + carouselBackground.getBackgroundTintList().getDefaultColor(), + activity.getColor(item.bgColor) + ); + colorAnim.setDuration(duration); + colorAnim.addUpdateListener(animator -> { + carouselBackground.setBackgroundTintList(ColorStateList.valueOf((int) animator.getAnimatedValue())); + }); + colorAnim.start(); + + View[] viewsToAnimate = {carouselTitle, carouselDesc, carouselButton, carouselIconContainer}; + for (View view : viewsToAnimate) { + view.animate().translationX(moveOutX).alpha(0f).setDuration(duration / 2) + .setInterpolator(new AccelerateInterpolator()) + .withEndAction(() -> { + if (view == carouselTitle) updateViews.run(); + view.setTranslationX(moveInX); + view.animate().translationX(0f).alpha(1f).setDuration(duration / 2) + .setInterpolator(new DecelerateInterpolator()) + .start(); + }).start(); + } + } else { + updateViews.run(); + } + } + + private void setupCarouselIndicators(int count) { + if (carouselIndicators == null) return; + carouselIndicators.removeAllViews(); + for (int i = 0; i < count; i++) { + ImageView dot = new ImageView(activity); + dot.setImageDrawable(activity.getDrawable(R.drawable.shape_dot_inactive)); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(16, 16); + params.setMargins(8, 0, 8, 0); + dot.setLayoutParams(params); + carouselIndicators.addView(dot); + } + } + + private void updateCarouselIndicators(int position) { + if (carouselIndicators == null) return; + for (int i = 0; i < carouselIndicators.getChildCount(); i++) { + ImageView dot = (ImageView) carouselIndicators.getChildAt(i); + if (i == position) { + dot.setImageDrawable(activity.getDrawable(R.drawable.shape_dot_active)); + } else { + dot.setImageDrawable(activity.getDrawable(R.drawable.shape_dot_inactive)); + } + } + } + + public void updateCarouselContext(boolean authorized) { + if (carouselBackground == null || carouselItems == null) return; + + boolean animate = !isFirstCarouselRender; + isFirstCarouselRender = false; + + if (!authorized) { + if (currentCarouselIndex != 0 || !animate) { + currentCarouselIndex = 0; + renderCarouselPage(0, animate, -1); + } + } else { + if (currentCarouselIndex != 1 || !animate) { + currentCarouselIndex = 1; + renderCarouselPage(1, animate, 1); + } + } + } +} From 0b147887374d5138b3e2d68aa63307a41b0cd019 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:24:49 +0530 Subject: [PATCH 010/394] Add DialogHelper for managing popup dialogs - Created a utility class to centralize the creation of system and alert dialogs. - Moved permission prompts and diagnostic popups out of the main activity file. - Documented the helper methods clearly for future maintainability. --- .../networktoggle/ui/DialogHelper.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/ui/DialogHelper.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/DialogHelper.java b/app/src/main/java/com/dhangofa/networktoggle/ui/DialogHelper.java new file mode 100644 index 0000000..a451e7d --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/DialogHelper.java @@ -0,0 +1,84 @@ +package com.dhangofa.networktoggle.ui; + +/** + * Helper to build and show the various popup dialogs in the app + * (like the permission bottom sheet or the diagnostic error dialog). + */ + +import android.app.Activity; +import android.app.Dialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Build; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.TextView; +import android.widget.Toast; + +import com.dhangofa.networktoggle.R; +import com.dhangofa.networktoggle.config.AppPreferences; +import com.dhangofa.networktoggle.telephony.SimResolver; +import com.dhangofa.networktoggle.util.DiagnosticReporter; + +public final class DialogHelper { + + private DialogHelper() {} + + public static Dialog buildPermissionBottomSheet(Activity activity, Runnable onGrantClicked) { + Dialog dialog = new Dialog(activity, R.style.TransparentBottomSheetStyle); + View view = activity.getLayoutInflater().inflate(R.layout.bottom_sheet_permission, null); + + view.findViewById(R.id.btnDismissPermission).setOnClickListener(v -> dialog.dismiss()); + view.findViewById(R.id.btnGrantPermission).setOnClickListener(v -> { + dialog.dismiss(); + onGrantClicked.run(); + }); + + dialog.setContentView(view); + Window window = dialog.getWindow(); + if (window != null) { + window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + window.setGravity(Gravity.BOTTOM); + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + } + return dialog; + } + + public static Dialog buildDiagnosticDialog(Activity activity, AppPreferences appPreferences, SimResolver simResolver, Runnable onClose) { + Dialog dialog = new Dialog(activity); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + dialog.setContentView(R.layout.dialog_diagnostic); + dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + dialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + TextView reportText = dialog.findViewById(R.id.diagnosticReportText); + if (reportText != null) { + String report = DiagnosticReporter.generateReport(appPreferences, simResolver); + reportText.setText(report); + } + + View btnClose = dialog.findViewById(R.id.btnCloseDiagnostic); + if (btnClose != null) { + btnClose.setOnClickListener(v -> { + dialog.dismiss(); + onClose.run(); + }); + } + + View btnCopy = dialog.findViewById(R.id.btnCopyDiagnostic); + if (btnCopy != null) { + btnCopy.setOnClickListener(v -> { + ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Activity.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Diagnostic Report", reportText.getText()); + clipboard.setPrimaryClip(clip); + Toast.makeText(activity, "Report copied to clipboard", Toast.LENGTH_SHORT).show(); + }); + } + + return dialog; + } +} From e6eb2a5ad3ec1377c33307d5ff99977e49ea4d7e Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:27:36 +0530 Subject: [PATCH 011/394] Add ExecutionStateController for managing execution state This class manages the lifecycle of Root shell pings and Shizuku binder callbacks, ensuring the UI status is synchronized with the execution backend state. It includes methods for checking root and Shizuku permissions, as well as registering and unregistering listeners. - Built a new controller to actively monitor Shizuku binder states and root availability. - Shifted the responsibility of updating the UI based on backend status into this isolated class. --- .../ui/ExecutionStateController.java | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/ui/ExecutionStateController.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/ExecutionStateController.java b/app/src/main/java/com/dhangofa/networktoggle/ui/ExecutionStateController.java new file mode 100644 index 0000000..ff90173 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/ExecutionStateController.java @@ -0,0 +1,161 @@ +package com.dhangofa.networktoggle.ui; + +/** + * Controller to manage the complex lifecycle of Root shell pings + * and Shizuku binder callbacks. It keeps the UI status text in sync with the actual + * execution backend state. + */ + +import android.app.Activity; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.service.quicksettings.TileService; + +import com.dhangofa.networktoggle.NetworkTileService; +import com.dhangofa.networktoggle.config.AppPreferences; +import com.dhangofa.networktoggle.model.ExecutionMode; + +import rikka.shizuku.Shizuku; + +public class ExecutionStateController { + private final Activity activity; + private final AppPreferences appPreferences; + private final StatusCallback statusCallback; + + private Thread rootCheckThread; + private Process rootCheckProcess; + private boolean isDestroyed = false; + + public interface StatusCallback { + void onStatusUpdate(String text, int color); + } + + private Shizuku.OnBinderReceivedListener binderReceivedListener; + private Shizuku.OnBinderDeadListener binderDeadListener; + private Shizuku.OnRequestPermissionResultListener permissionResultListener; + + public ExecutionStateController(Activity activity, AppPreferences appPreferences, StatusCallback statusCallback) { + this.activity = activity; + this.appPreferences = appPreferences; + this.statusCallback = statusCallback; + + binderReceivedListener = () -> + activity.runOnUiThread(() -> { + if (!isDestroyed && appPreferences != null + && appPreferences.getExecutionMode() == ExecutionMode.SHIZUKU) { + checkShizukuPermission(false); + } + }); + + binderDeadListener = () -> + activity.runOnUiThread(() -> { + if (!isDestroyed && appPreferences != null + && appPreferences.getExecutionMode() == ExecutionMode.SHIZUKU) { + statusCallback.onStatusUpdate("Shizuku is not running.", 0xFFFF5555); + } + }); + + permissionResultListener = + (requestCode, grantResult) -> activity.runOnUiThread(() -> { + if (!isDestroyed && appPreferences != null + && appPreferences.getExecutionMode() == ExecutionMode.SHIZUKU) { + checkShizukuPermission(false); + } + }); + } + + public void registerListeners() { + Shizuku.addBinderReceivedListener(binderReceivedListener); + Shizuku.addBinderDeadListener(binderDeadListener); + Shizuku.addRequestPermissionResultListener(permissionResultListener); + } + + public void unregisterListeners() { + Shizuku.removeBinderReceivedListener(binderReceivedListener); + Shizuku.removeBinderDeadListener(binderDeadListener); + Shizuku.removeRequestPermissionResultListener(permissionResultListener); + } + + public void destroy() { + isDestroyed = true; + unregisterListeners(); + if (rootCheckProcess != null) { + rootCheckProcess.destroy(); + rootCheckProcess = null; + } + if (rootCheckThread != null && rootCheckThread.isAlive()) { + rootCheckThread.interrupt(); + rootCheckThread = null; + } + } + + public void checkRootPermission() { + statusCallback.onStatusUpdate("Checking root permission...", 0xFFFFB300); + rootCheckThread = new Thread(() -> { + boolean granted = false; + Process process = null; + try { + process = Runtime.getRuntime().exec(new String[]{"su", "-c", "id"}); + rootCheckProcess = process; + granted = process.waitFor() == 0; + } catch (Exception ignored) { + granted = false; + } finally { + if (process != null) process.destroy(); + if (rootCheckProcess == process) rootCheckProcess = null; + } + + boolean finalGranted = granted; + activity.runOnUiThread(() -> { + if (isDestroyed || appPreferences == null + || appPreferences.getExecutionMode() != ExecutionMode.ROOT) return; + if (finalGranted) { + statusCallback.onStatusUpdate("Root mode active & authorized!", 0xFF1B873F); + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_NONE); + } else { + statusCallback.onStatusUpdate("Root permission denied or unavailable.", 0xFFFF5555); + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_ROOT); + } + TileService.requestListeningState(activity, new ComponentName(activity, NetworkTileService.class)); + }); + }); + rootCheckThread.start(); + } + + public void checkShizukuPermission(boolean requestIfNeeded) { + if (isDestroyed) return; + try { + if (!Shizuku.pingBinder()) { + statusCallback.onStatusUpdate("Shizuku is not running.", 0xFFFF5555); + if (appPreferences != null) { + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_SHIZUKU); + TileService.requestListeningState(activity, new ComponentName(activity, NetworkTileService.class)); + } + return; + } + if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { + statusCallback.onStatusUpdate("Shizuku mode active & authorized!", 0xFF1B873F); + if (appPreferences != null) { + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_NONE); + TileService.requestListeningState(activity, new ComponentName(activity, NetworkTileService.class)); + } + return; + } + statusCallback.onStatusUpdate("Shizuku permission not granted.", 0xFFFFB300); + if (appPreferences != null) { + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_SHIZUKU); + TileService.requestListeningState(activity, new ComponentName(activity, NetworkTileService.class)); + } + if (requestIfNeeded) { + statusCallback.onStatusUpdate("Requesting Shizuku permission...", 0xFFFFB300); + Shizuku.requestPermission(0); + } + } catch (Exception e) { + statusCallback.onStatusUpdate("Shizuku check failed.", 0xFFFF5555); + if (appPreferences != null) { + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_SHIZUKU); + TileService.requestListeningState(activity, new ComponentName(activity, NetworkTileService.class)); + } + } + } +} From adda4b4b553885e0ee13480db2c50497ed7dc023 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:30:22 +0530 Subject: [PATCH 012/394] Implement PhoneStatePermissionManager for permission handling - Separated the READ_PHONE_STATE Android permission request flow into a dedicated manager. - Handled capability resolution callbacks securely within this new structure. --- .../ui/PhoneStatePermissionManager.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/ui/PhoneStatePermissionManager.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/PhoneStatePermissionManager.java b/app/src/main/java/com/dhangofa/networktoggle/ui/PhoneStatePermissionManager.java new file mode 100644 index 0000000..261d5fc --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/PhoneStatePermissionManager.java @@ -0,0 +1,87 @@ +package com.dhangofa.networktoggle.ui; + +/** + * Manager to handle the READ_PHONE_STATE permission flow. + * It tracks SDK-version checks, shows the rationale bottom-sheet UI, and routes + * the onRequestPermissionsResult cleanly. + */ + +import android.app.Activity; +import android.app.Dialog; +import android.content.pm.PackageManager; +import android.os.Build; + +public class PhoneStatePermissionManager { + + private final Activity activity; + private final int reqCode; + private final Runnable onGranted; + private Dialog permissionDialog; + private boolean activityDestroyed; + + public PhoneStatePermissionManager(Activity activity, int reqCode, Runnable onGranted) { + this.activity = activity; + this.reqCode = reqCode; + this.onGranted = onGranted; + this.activityDestroyed = false; + } + + public void checkAndRequest() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + boolean granted = activity.checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED; + if (!granted) { + boolean shouldShowRationale = activity.shouldShowRequestPermissionRationale(android.Manifest.permission.READ_PHONE_STATE); + if (shouldShowRationale) { + showPermissionBottomSheet(); + } else { + activity.requestPermissions(new String[]{android.Manifest.permission.READ_PHONE_STATE}, reqCode); + } + } else { + if (permissionDialog != null && permissionDialog.isShowing()) { + permissionDialog.dismiss(); + } + if (onGranted != null) { + onGranted.run(); + } + } + } else { + if (onGranted != null) { + onGranted.run(); + } + } + } + + private void showPermissionBottomSheet() { + if (permissionDialog == null) { + permissionDialog = DialogHelper.buildPermissionBottomSheet(activity, () -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + activity.requestPermissions(new String[]{android.Manifest.permission.READ_PHONE_STATE}, reqCode); + } + }); + } + + if (!permissionDialog.isShowing() && !activityDestroyed) { + permissionDialog.show(); + } + } + + public void handleRequestPermissionsResult(int requestCode, int[] grantResults) { + if (requestCode == reqCode) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (permissionDialog != null && permissionDialog.isShowing()) permissionDialog.dismiss(); + if (onGranted != null) { + onGranted.run(); + } + } else { + showPermissionBottomSheet(); + } + } + } + + public void destroy() { + activityDestroyed = true; + if (permissionDialog != null && permissionDialog.isShowing()) { + permissionDialog.dismiss(); + } + } +} From e30f5795468356db67df58c175d13a73a24ad848 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:32:31 +0530 Subject: [PATCH 013/394] Implement TileIconManager for dynamic icon creation This class provides a utility to dynamically create Quick Settings tile icons by drawing text on a bitmap canvas instead of using static images. - - Extracted the dynamic canvas drawing logic for the Quick Settings tile into a specialized class. - Handled generating bitmaps and text overlays (e.g., '5G', '4G') to reflect network states visually. - Included third-person documentation outlining the rendering steps. --- .../networktoggle/ui/TileIconManager.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/ui/TileIconManager.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/TileIconManager.java b/app/src/main/java/com/dhangofa/networktoggle/ui/TileIconManager.java new file mode 100644 index 0000000..151065c --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/TileIconManager.java @@ -0,0 +1,75 @@ +package com.dhangofa.networktoggle.ui; + +/** + * Utility to dynamically draw the Quick Settings tile icons. + * Instead of having dozens of static image files, this draws the text (like "5G", "LTE") + * directly onto a blank icon canvas on the fly. + */ + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.graphics.drawable.Icon; + +public final class TileIconManager { + private static Icon icon4g; + private static Icon icon5g; + private static Icon iconP5g; + private static Icon iconP4g; + private static Icon iconP3g; + private static Icon icon2g; + private static Icon iconUnknown; + + private TileIconManager() {} + + private static Icon createTextOnlyIcon(String text) { + int size = 256; + Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setColor(Color.WHITE); + paint.setTypeface(Typeface.create("sans-serif-condensed", Typeface.BOLD)); + paint.setTextAlign(Paint.Align.CENTER); + paint.setTextSize(190f); + + float width = paint.measureText(text); + if (width > 240f) { + paint.setTextScaleX(240f / width); + } + + Paint.FontMetrics metrics = paint.getFontMetrics(); + float y = (size / 2f) - (metrics.descent + metrics.ascent) / 2f; + canvas.drawText(text, size / 2f, y, paint); + + return Icon.createWithBitmap(bitmap); + } + + public static Icon getCachedIcon(String text) { + switch (text) { + case "4G": + if (icon4g == null) icon4g = createTextOnlyIcon("4G"); + return icon4g; + case "5G": + if (icon5g == null) icon5g = createTextOnlyIcon("5G"); + return icon5g; + case "P5G": + if (iconP5g == null) iconP5g = createTextOnlyIcon("P5G"); + return iconP5g; + case "P4G": + if (iconP4g == null) iconP4g = createTextOnlyIcon("P4G"); + return iconP4g; + case "P3G": + if (iconP3g == null) iconP3g = createTextOnlyIcon("P3G"); + return iconP3g; + case "2G": + if (icon2g == null) icon2g = createTextOnlyIcon("2G"); + return icon2g; + default: + if (iconUnknown == null) iconUnknown = createTextOnlyIcon("?"); + return iconUnknown; + } + } +} From 9b06907f68085fcf84572e6bf526abd919c5ea55 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:40:11 +0530 Subject: [PATCH 014/394] Add hiddenapibypass dependency to build.gradle.kts - Adjusted module dependencies to support the newly modularized architecture. - Verified compilation parameters for the refactored project structure. --- app/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fc8d1ae..fb32506 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -43,6 +43,7 @@ android { } dependencies { + implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3") // Shizuku API implementation("dev.rikka.shizuku:api:13.1.5") From 38653fd040f76bb95ba5ddcce3de77d232975d0f Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:41:37 +0530 Subject: [PATCH 015/394] Add documentation for BootReceiver class Added documentation for BootReceiver class to explain its purpose and functionality. --- .../main/java/com/dhangofa/networktoggle/BootReceiver.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/BootReceiver.java b/app/src/main/java/com/dhangofa/networktoggle/BootReceiver.java index 99ba90b..7b8f365 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/BootReceiver.java +++ b/app/src/main/java/com/dhangofa/networktoggle/BootReceiver.java @@ -1,5 +1,10 @@ package com.dhangofa.networktoggle; +/** + * Broadcast receiver for system boot and app update events. + * Clears out any stale transient state (like temporary errors or cached + * network modes) so the app can start completely fresh after a device reboot or app update. + */ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; From 8520dc33c4cd8a26c41d77a93b396d3df83cb3d8 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:43:15 +0530 Subject: [PATCH 016/394] Add Javadoc for CommandExecutorFactory Added documentation for CommandExecutorFactory class. --- .../networktoggle/command/CommandExecutorFactory.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutorFactory.java b/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutorFactory.java index 150a4d3..b69603b 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutorFactory.java +++ b/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutorFactory.java @@ -1,5 +1,10 @@ package com.dhangofa.networktoggle.command; +/** + * Factory class used to create the right CommandExecutor. + * It looks at the user's preferred execution mode (Root vs Shizuku) and returns the corresponding executor. + */ + import com.dhangofa.networktoggle.model.ExecutionMode; public final class CommandExecutorFactory { From 0a8695046182d4442f2f9c7a10e0d267b6db6207 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:44:35 +0530 Subject: [PATCH 017/394] Add Javadoc for CommandExecutor interface Added documentation for CommandExecutor interface. --- .../com/dhangofa/networktoggle/command/CommandExecutor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutor.java b/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutor.java index 66cbeab..6c8a372 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutor.java +++ b/app/src/main/java/com/dhangofa/networktoggle/command/CommandExecutor.java @@ -1,5 +1,10 @@ package com.dhangofa.networktoggle.command; +/** + * Interface for executing shell commands. + * Provides a common contract to swap between Root and Shizuku implementations seamlessly. + */ + import com.dhangofa.networktoggle.model.CommandResult; public interface CommandExecutor { From 9f9fd49001bae0e3743419daea4d559d049ac7af Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:46:39 +0530 Subject: [PATCH 018/394] Document ProcessResultReader functionality Added a class-level comment explaining the purpose of the ProcessResultReader class and its functionality to handle process output streams. --- .../dhangofa/networktoggle/command/ProcessResultReader.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/ProcessResultReader.java b/app/src/main/java/com/dhangofa/networktoggle/command/ProcessResultReader.java index 3397b81..8a2e9c3 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/command/ProcessResultReader.java +++ b/app/src/main/java/com/dhangofa/networktoggle/command/ProcessResultReader.java @@ -1,5 +1,10 @@ package com.dhangofa.networktoggle.command; +/** + * Helper to consume standard output and error streams from a process. + * This prevents deadlocks that can happen if the output buffer fills up before it is read. + */ + import com.dhangofa.networktoggle.model.CommandResult; final class ProcessResultReader { From 8e76c2090b360e6a7a15716accc263839a39b9ae Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:47:34 +0530 Subject: [PATCH 019/394] Add Javadoc for RootCommandExecutor implementation Added documentation for RootCommandExecutor class. --- .../dhangofa/networktoggle/command/RootCommandExecutor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/RootCommandExecutor.java b/app/src/main/java/com/dhangofa/networktoggle/command/RootCommandExecutor.java index 4d53596..6f1c678 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/command/RootCommandExecutor.java +++ b/app/src/main/java/com/dhangofa/networktoggle/command/RootCommandExecutor.java @@ -1,5 +1,10 @@ package com.dhangofa.networktoggle.command; +/** + * Implementation of CommandExecutor that uses standard su (Root) privileges. + * It spawns a root shell, runs the command, and collects the output via ProcessResultReader. + */ + import com.dhangofa.networktoggle.model.CommandResult; public final class RootCommandExecutor implements CommandExecutor { From bd04d27f8cd305509f3edaa53c12c370b30c493a Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:48:24 +0530 Subject: [PATCH 020/394] Add Javadoc comments to ShizukuCommandExecutor Added documentation for ShizukuCommandExecutor class. --- .../networktoggle/command/ShizukuCommandExecutor.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/command/ShizukuCommandExecutor.java b/app/src/main/java/com/dhangofa/networktoggle/command/ShizukuCommandExecutor.java index bce624a..07141e2 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/command/ShizukuCommandExecutor.java +++ b/app/src/main/java/com/dhangofa/networktoggle/command/ShizukuCommandExecutor.java @@ -1,5 +1,11 @@ package com.dhangofa.networktoggle.command; +/** + * Implementation of CommandExecutor that relies on Shizuku for elevated privileges. + * This is great for users who don't have a rooted device but want to use ADB-level commands. + * It hooks into the Shizuku Binder to execute the shell command. + */ + import android.content.pm.PackageManager; import com.dhangofa.networktoggle.model.CommandResult; import java.lang.reflect.Method; From 458a3977b04ed7acb173784494773dd9479bf1a6 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:49:13 +0530 Subject: [PATCH 021/394] Add Javadoc comments to StreamCollector class Added documentation for StreamCollector class. --- .../com/dhangofa/networktoggle/command/StreamCollector.java | 5 +++++ 1 file changed, 5 insertions(+) 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 efaca66..d31f9ff 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/command/StreamCollector.java +++ b/app/src/main/java/com/dhangofa/networktoggle/command/StreamCollector.java @@ -1,5 +1,10 @@ package com.dhangofa.networktoggle.command; +/** + * Utility class used internally by ProcessResultReader to asynchronously read an InputStream + * into a String. It runs on a separate thread to keep things non-blocking. + */ + import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; From 30c376fb66991f73c18f137af24bdab9e705de74 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:51:45 +0530 Subject: [PATCH 022/394] Implement last network check timestamp methods - Added support for storing execution timestamps to track the last network state check. --- .../networktoggle/config/AppPreferences.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java b/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java index d56dd31..988b24d 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java +++ b/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java @@ -1,5 +1,12 @@ package com.dhangofa.networktoggle.config; +/** + * Central configuration and state manager. + * Reads and writes all the SharedPreferences for the app, including which execution mode + * (Root/Shizuku) is selected, target SIM, saved network cycles, and any transient error states. + * This acts as the single source of truth for app settings. + */ + import android.content.Context; import android.content.SharedPreferences; @@ -15,6 +22,7 @@ public final class AppPreferences { private static final String KEY_EXEC_MODE = "exec_mode"; private static final String KEY_TARGET_SIM = "target_sim"; private static final String KEY_NETWORK_STATE = "net_state"; + private static final String KEY_LAST_NETWORK_CHECK = "last_network_check"; private static final String KEY_AUTO_SIM_ERROR = "auto_sim_error"; private static final String KEY_TILE_CYCLE_MODES = "tile_cycle_modes"; @@ -79,6 +87,14 @@ public void clearCachedNetworkMode() { setCachedNetworkMode(NetworkMode.UNKNOWN); } + public long getLastNetworkCheckTimestamp() { + return preferences.getLong(KEY_LAST_NETWORK_CHECK, 0); + } + + public void setLastNetworkCheckTimestamp(long timestamp) { + preferences.edit().putLong(KEY_LAST_NETWORK_CHECK, timestamp).apply(); + } + public boolean hasAutoSimError() { return preferences.getBoolean(KEY_AUTO_SIM_ERROR, false); } From 9c3f5e0c0b5b32643de3753660979d013c7025f0 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:53:02 +0530 Subject: [PATCH 023/394] Add documentation for TileCycleManager class Added documentation for TileCycleManager class to explain its purpose and functionality. --- .../com/dhangofa/networktoggle/cycle/TileCycleManager.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/cycle/TileCycleManager.java b/app/src/main/java/com/dhangofa/networktoggle/cycle/TileCycleManager.java index 2c007d1..314364b 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/cycle/TileCycleManager.java +++ b/app/src/main/java/com/dhangofa/networktoggle/cycle/TileCycleManager.java @@ -1,5 +1,11 @@ package com.dhangofa.networktoggle.cycle; +/** + * This handles the logic for the "cycle" of network modes. + * When tapping the Quick Settings tile, this class is used to figure out what the next mode + * in the sequence should be based on preferences. + */ + import com.dhangofa.networktoggle.config.AppPreferences; import com.dhangofa.networktoggle.model.NetworkMode; From e1f2616cc92f78615eec6e33a9b87b5669b0af80 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:56:47 +0530 Subject: [PATCH 024/394] Modularize MainActivity UI - Reduced class complexity by removing carousel, dialog, and permission logic. - Instantiated the new CarouselManager, DialogHelper, and ExecutionStateController classes. --- .../dhangofa/networktoggle/MainActivity.java | 460 ++---------------- 1 file changed, 54 insertions(+), 406 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java index b2da717..95d6fc7 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java +++ b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java @@ -1,22 +1,17 @@ -/* -Behavior: -- Fresh install: no execution mode 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. - -Target SIM behavior: -- Target SIM defaults to Auto. -- Auto mode is handled by the QS tile service using the active data subscription. -- Auto mode resolves the active data subscription to the physical SIM slot before applying changes. -- If Auto SIM detection fails, the QS tile sets auto_sim_error=true and shows a Toast. -- MainActivity shows a persistent warning when auto_sim_error=true. -- Selecting any Target SIM option clears the previous Auto SIM warning. -- SIM 1 and SIM 2 use the chosen physical SIM slot directly. -*/ - +/** + * The main configuration screen for the app. + * Orchestrates all the setup pieces: choosing the backend (Root or Shizuku), + * picking the target SIM, managing the onboarding carousel UI, and keeping permissions in check. + * + * Notes on behavior for future reference: + * - On a fresh install, no execution mode is selected yet. + * - When Root is selected, it immediately checks the su binary. + * - When Shizuku is selected, it checks/requests the Shizuku permission. + * - Shizuku callbacks only update the UI when Shizuku mode is actually selected. + * - Target SIM defaults to Auto, which resolves the active data subscription to the physical slot. + * - If Auto SIM detection fails, the QS tile flags it and this screen shows a persistent warning. + * - Selecting a specific SIM slot clears that Auto SIM warning. + */ package com.dhangofa.networktoggle; import android.app.Activity; @@ -43,6 +38,7 @@ import com.dhangofa.networktoggle.telephony.NetworkCapabilityResolver; import com.dhangofa.networktoggle.telephony.SimResolver; import com.dhangofa.networktoggle.ui.TileCycleUiController; +import com.dhangofa.networktoggle.ui.DialogHelper; import android.app.Dialog; import android.view.Gravity; import android.view.ViewGroup; @@ -77,34 +73,13 @@ public class MainActivity extends Activity implements android.content.SharedPref private ImageView faqLink; // Morphing View Carousel - private View carouselBackground; - private TextView carouselTitle; - private TextView carouselDesc; - private TextView carouselButtonText; - private ImageView carouselIcon; - private ImageView carouselButtonIcon; - private View carouselButton; - private View carouselIconContainer; - private LinearLayout carouselIndicators; - - private int currentCarouselIndex = 0; - private float touchStartX = 0f; - private CarouselItem[] carouselItems; - - private static class CarouselItem { - String title, desc, btnText, url; - int iconRes, bgColor, accentColor, btnBgColor; - CarouselItem(String title, String desc, String btnText, String url, int iconRes, int bgColor, int accentColor, int btnBgColor) { - this.title = title; this.desc = desc; this.btnText = btnText; this.url = url; - this.iconRes = iconRes; this.bgColor = bgColor; this.accentColor = accentColor; this.btnBgColor = btnBgColor; - } - } - + private com.dhangofa.networktoggle.ui.CarouselManager carouselManager; + private View separatorRootShizuku; private View separatorAutoSim1; private View separatorSim1Sim2; - private Dialog permissionDialog; + private com.dhangofa.networktoggle.ui.PhoneStatePermissionManager permissionManager; private View errorBannerContainer; private Dialog diagnosticDialog; private NetworkCapabilityResolver capabilityResolver; @@ -114,35 +89,7 @@ private static class CarouselItem { private AppPreferences appPreferences; private TileCycleUiController tileCycleUiController; private volatile boolean activityDestroyed; - private Thread rootCheckThread; - private Process rootCheckProcess; - - private final Shizuku.OnBinderReceivedListener binderReceivedListener = () -> - runOnUiThread(() -> { - if (!activityDestroyed - && appPreferences != null - && appPreferences.getExecutionMode() == ExecutionMode.SHIZUKU) { - checkShizukuPermission(false); - } - }); - - private final Shizuku.OnBinderDeadListener binderDeadListener = () -> - runOnUiThread(() -> { - if (!activityDestroyed - && appPreferences != null - && appPreferences.getExecutionMode() == ExecutionMode.SHIZUKU) { - setStatus("Shizuku is not running.", 0xFFFF5555); - } - }); - - private final Shizuku.OnRequestPermissionResultListener permissionResultListener = - (requestCode, grantResult) -> runOnUiThread(() -> { - if (!activityDestroyed - && appPreferences != null - && appPreferences.getExecutionMode() == ExecutionMode.SHIZUKU) { - checkShizukuPermission(false); - } - }); + private com.dhangofa.networktoggle.ui.ExecutionStateController executionStateController; @Override protected void onCreate(Bundle savedInstanceState) { @@ -163,6 +110,10 @@ protected void onCreate(Bundle savedInstanceState) { tileCycleUiController = new TileCycleUiController(this, tileCycleManager); NetworkModeController modeController = new NetworkModeController(simResolver); + permissionManager = new com.dhangofa.networktoggle.ui.PhoneStatePermissionManager(this, REQ_CODE_PHONE_STATE, this::updateCapabilities); + + executionStateController = new com.dhangofa.networktoggle.ui.ExecutionStateController(this, appPreferences, this::setStatus); + tileCycleUiController.setOnCycleChangedListener(newCycle -> { new Thread(() -> { NetworkMode currentMode = appPreferences.getCachedNetworkMode(); @@ -170,13 +121,20 @@ protected void onCreate(Bundle savedInstanceState) { // If cache is wiped (e.g. from SIM switch), but execution is allowed, read it directly once if (currentMode == NetworkMode.UNKNOWN && appPreferences.getExecutionMode() != ExecutionMode.NONE) { currentMode = new com.dhangofa.networktoggle.telephony.NetworkModeReader(this, appPreferences, simResolver).readCurrentMode(); + if (currentMode != NetworkMode.UNKNOWN) { + appPreferences.setLastNetworkCheckTimestamp(System.currentTimeMillis()); + appPreferences.setCachedNetworkMode(currentMode); + } } if (currentMode != NetworkMode.UNKNOWN && !newCycle.contains(currentMode)) { NetworkMode fallbackMode = newCycle.get(0); // Attempt to sync the network state silently - modeController.apply(fallbackMode, appPreferences.getExecutionMode()); - appPreferences.setCachedNetworkMode(fallbackMode); + com.dhangofa.networktoggle.model.CommandResult result = modeController.apply(fallbackMode, appPreferences.getExecutionMode()); + if (result.isSuccess()) { + appPreferences.setCachedNetworkMode(fallbackMode); + appPreferences.setLastNetworkCheckTimestamp(System.currentTimeMillis()); + } } else if (currentMode != NetworkMode.UNKNOWN) { // It's in the cycle, ensure it is cached so UI shows the actual active state appPreferences.setCachedNetworkMode(currentMode); @@ -185,13 +143,14 @@ protected void onCreate(Bundle savedInstanceState) { }); tileCycleUiController.initialize(); - registerShizukuListeners(); + executionStateController.registerListeners(); loadSavedExecutionMode(); loadSavedTargetSimMode(); updateAutoSimWarning(); bindSelectionListeners(); updateSeparatorVisibility(); - setupCarousel(); + carouselManager = new com.dhangofa.networktoggle.ui.CarouselManager(this); + carouselManager.setupCarousel(isUIAuthorized); setupStaticLandscapeCards(); } @@ -220,15 +179,6 @@ private void bindViews() { telegramLink = findViewById(R.id.telegramLink); faqLink = findViewById(R.id.faqLink); - carouselBackground = findViewById(R.id.carouselBackground); - carouselTitle = findViewById(R.id.carouselTitle); - carouselDesc = findViewById(R.id.carouselDesc); - carouselButtonText = findViewById(R.id.carouselButtonText); - carouselIcon = findViewById(R.id.carouselIcon); - carouselButtonIcon = findViewById(R.id.carouselButtonIcon); - carouselButton = findViewById(R.id.carouselButton); - carouselIconContainer = findViewById(R.id.carouselIconContainer); - carouselIndicators = findViewById(R.id.carouselIndicators); separatorRootShizuku = findViewById(R.id.separatorRootShizuku); separatorAutoSim1 = findViewById(R.id.separatorAutoSim1); separatorSim1Sim2 = findViewById(R.id.separatorSim1Sim2); @@ -263,137 +213,6 @@ private void setupStaticLandscapeCards() { } } - private void setupCarousel() { - if (carouselBackground == null) return; - carouselItems = new CarouselItem[]{ - new CarouselItem("New to NetToggle?", "Learn how to set up Execution Modes", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/1.-Execution-Mode-Configuration", R.drawable.ic_terminal, R.color.first_pg_bg, R.color.exec_accent, R.color.view_guide_button_bg), - new CarouselItem("Target SIM & Cycle", "Learn how to configure your modes", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/2.-Target-SIM-Setup-&-Quick-Tile-Cycle-Guide", R.drawable.ic_sim_card, R.color.second_pg_bg, R.color.accent_orange, R.color.view_guide_button_bg), - new CarouselItem("Quick Settings Ready", "Add the tile to your Control Center", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/3.-Adding-the-Tile-to-Quick-Settings", R.drawable.ic_network_bars, R.color.third_pg_bg, R.color.accent_pink, R.color.view_guide_button_bg) - }; - - setupCarouselIndicators(carouselItems.length); - // Start state depends on initial auth state. - // We defer to updateCarouselContext which will be triggered by loadSavedExecutionMode -> setStatus -> updateAuthorizationUI - // But let's set a default page just in case - renderCarouselPage(isUIAuthorized ? 1 : 0, false, 0); - - carouselBackground.setOnTouchListener((v, event) -> { - switch (event.getAction()) { - case android.view.MotionEvent.ACTION_DOWN: - touchStartX = event.getX(); - return true; - case android.view.MotionEvent.ACTION_UP: - case android.view.MotionEvent.ACTION_CANCEL: - float deltaX = event.getX() - touchStartX; - if (Math.abs(deltaX) > 100) { - if (deltaX > 0) { - // Swipe right (previous page) - currentCarouselIndex = (currentCarouselIndex - 1 + carouselItems.length) % carouselItems.length; - renderCarouselPage(currentCarouselIndex, true, -1); - } else if (deltaX < 0) { - // Swipe left (next page) - currentCarouselIndex = (currentCarouselIndex + 1) % carouselItems.length; - renderCarouselPage(currentCarouselIndex, true, 1); - } - - - } else if (event.getAction() == android.view.MotionEvent.ACTION_UP) { - v.performClick(); - } - return true; - } - return false; - }); - - carouselBackground.setOnClickListener(v -> { - // Needed to ensure ripple works when performClick is called - }); - } - - private void renderCarouselPage(int index, boolean animate, int direction) { - CarouselItem item = carouselItems[index]; - Runnable updateViews = () -> { - carouselTitle.setText(item.title); - carouselDesc.setText(item.desc); - carouselButtonText.setText(item.btnText); - carouselButtonText.setTextColor(getColor(item.accentColor)); - - carouselIcon.setImageResource(item.iconRes); - carouselIcon.setColorFilter(getColor(item.accentColor)); - carouselButtonIcon.setColorFilter(getColor(item.accentColor)); - - carouselBackground.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getColor(item.bgColor))); - carouselButton.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getColor(item.btnBgColor))); - carouselIconContainer.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getColor(item.btnBgColor))); - - carouselButton.setOnClickListener(v -> { - try { startActivity(new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(item.url))); } catch (Exception e) {} - }); - updateCarouselIndicators(index); - }; - - if (animate) { - float moveOutX = direction * -100f; // Swipe left (next) -> content moves left (-100). - float moveInX = direction * 100f; // Swipe left -> new content comes from right (+100). - long duration = 200; - - // Animate background color smoothly - android.animation.ValueAnimator colorAnim = android.animation.ValueAnimator.ofObject( - new android.animation.ArgbEvaluator(), - carouselBackground.getBackgroundTintList().getDefaultColor(), - getColor(item.bgColor) - ); - colorAnim.setDuration(duration); - colorAnim.addUpdateListener(animator -> { - carouselBackground.setBackgroundTintList(android.content.res.ColorStateList.valueOf((int) animator.getAnimatedValue())); - }); - colorAnim.start(); - - // Animate content out - android.view.View[] viewsToAnimate = {carouselTitle, carouselDesc, carouselButton, carouselIconContainer}; - for (android.view.View view : viewsToAnimate) { - view.animate().translationX(moveOutX).alpha(0f).setDuration(duration / 2) - .setInterpolator(new android.view.animation.AccelerateInterpolator()) - .withEndAction(() -> { - // Once out, swap data - if (view == carouselTitle) updateViews.run(); - - // Prepare for animate in - view.setTranslationX(moveInX); - view.animate().translationX(0f).alpha(1f).setDuration(duration / 2) - .setInterpolator(new android.view.animation.DecelerateInterpolator()) - .start(); - }).start(); - } - } else { - updateViews.run(); - } - } - - private void setupCarouselIndicators(int count) { - if (carouselIndicators == null) return; - carouselIndicators.removeAllViews(); - for (int i = 0; i < count; i++) { - android.widget.ImageView dot = new android.widget.ImageView(this); - dot.setImageDrawable(getDrawable(R.drawable.shape_dot_inactive)); - android.widget.LinearLayout.LayoutParams params = new android.widget.LinearLayout.LayoutParams(16, 16); - params.setMargins(8, 0, 8, 0); - dot.setLayoutParams(params); - carouselIndicators.addView(dot); - } - } - - private void updateCarouselIndicators(int position) { - if (carouselIndicators == null) return; - for (int i = 0; i < carouselIndicators.getChildCount(); i++) { - android.widget.ImageView dot = (android.widget.ImageView) carouselIndicators.getChildAt(i); - if (i == position) { - dot.setImageDrawable(getDrawable(R.drawable.shape_dot_active)); - } else { - dot.setImageDrawable(getDrawable(R.drawable.shape_dot_inactive)); - } - } - } private void updateSeparatorVisibility() { // Execution Mode Separator int modeId = radioGroup.getCheckedRadioButtonId(); @@ -420,20 +239,14 @@ private void updateSeparatorVisibility() { } } - private void registerShizukuListeners() { - Shizuku.addBinderReceivedListener(binderReceivedListener); - Shizuku.addBinderDeadListener(binderDeadListener); - Shizuku.addRequestPermissionResultListener(permissionResultListener); - } - private void loadSavedExecutionMode() { ExecutionMode savedMode = appPreferences.getExecutionMode(); if (savedMode == ExecutionMode.ROOT) { radioRoot.setChecked(true); - checkRootPermission(); + executionStateController.checkRootPermission(); } else if (savedMode == ExecutionMode.SHIZUKU) { radioShizuku.setChecked(true); - checkShizukuPermission(false); + executionStateController.checkShizukuPermission(false); } else { radioGroup.clearCheck(); setStatus("Select Root or Shizuku mode.", 0xFFFFB300); @@ -447,10 +260,10 @@ private void bindSelectionListeners() { updateSeparatorVisibility(); if (checkedId == R.id.radioRoot) { appPreferences.onExecutionModeChanged(ExecutionMode.ROOT); - checkRootPermission(); + executionStateController.checkRootPermission(); } else if (checkedId == R.id.radioShizuku) { appPreferences.onExecutionModeChanged(ExecutionMode.SHIZUKU); - checkShizukuPermission(true); + executionStateController.checkShizukuPermission(true); } }); @@ -507,10 +320,8 @@ protected void onResume() { super.onResume(); if (appPreferences != null) updateAutoSimWarning(); - checkAndRequestPermission(); + if (permissionManager != null) permissionManager.checkAndRequest(); updateErrorBanner(); - updateCapabilities(); - } @Override @@ -518,50 +329,6 @@ protected void onPause() { super.onPause(); } - private void checkAndRequestPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - boolean granted = checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED; - if (!granted) { - boolean shouldShowRationale = shouldShowRequestPermissionRationale(android.Manifest.permission.READ_PHONE_STATE); - if (shouldShowRationale) { - showPermissionBottomSheet(); - } else { - // Automatically prompt on first open if not asked yet - requestPermissions(new String[]{android.Manifest.permission.READ_PHONE_STATE}, REQ_CODE_PHONE_STATE); - } - } else if (permissionDialog != null && permissionDialog.isShowing()) { - permissionDialog.dismiss(); - } - } - } - - private void showPermissionBottomSheet() { - if (permissionDialog == null) { - permissionDialog = new Dialog(this, R.style.TransparentBottomSheetStyle); - View view = getLayoutInflater().inflate(R.layout.bottom_sheet_permission, null); - - view.findViewById(R.id.btnDismissPermission).setOnClickListener(v -> permissionDialog.dismiss()); - view.findViewById(R.id.btnGrantPermission).setOnClickListener(v -> { - permissionDialog.dismiss(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(new String[]{android.Manifest.permission.READ_PHONE_STATE}, REQ_CODE_PHONE_STATE); - } - }); - - permissionDialog.setContentView(view); - Window window = permissionDialog.getWindow(); - if (window != null) { - window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - window.setGravity(Gravity.BOTTOM); - window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); - } - } - - if (!permissionDialog.isShowing() && !activityDestroyed) { - permissionDialog.show(); - } - } - private void updateErrorBanner() { if (errorBannerContainer != null && appPreferences != null) { DiagnosticError error = appPreferences.getLastError(); @@ -576,30 +343,10 @@ private void showDiagnosticDialog() { diagnosticDialog.dismiss(); appPreferences.clearLastError(); updateErrorBanner(); } - diagnosticDialog = new Dialog(this); - diagnosticDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - diagnosticDialog.setContentView(R.layout.dialog_diagnostic); - diagnosticDialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); - diagnosticDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - - TextView reportText = diagnosticDialog.findViewById(R.id.diagnosticReportText); - if (reportText != null) { - String report = DiagnosticReporter.generateReport(appPreferences, new SimResolver(this, appPreferences)); - reportText.setText(report); - } - - View btnClose = diagnosticDialog.findViewById(R.id.btnCloseDiagnostic); - if (btnClose != null) btnClose.setOnClickListener(v -> { diagnosticDialog.dismiss(); appPreferences.clearLastError(); updateErrorBanner(); }); - - View btnCopy = diagnosticDialog.findViewById(R.id.btnCopyDiagnostic); - if (btnCopy != null) { - btnCopy.setOnClickListener(v -> { - ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Diagnostic Report", reportText.getText()); - clipboard.setPrimaryClip(clip); - Toast.makeText(this, "Report copied to clipboard", Toast.LENGTH_SHORT).show(); - }); - } + diagnosticDialog = DialogHelper.buildDiagnosticDialog(this, appPreferences, new SimResolver(this, appPreferences), () -> { + appPreferences.clearLastError(); + updateErrorBanner(); + }); diagnosticDialog.show(); } @@ -618,13 +365,8 @@ private void updateCapabilities() { @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requestCode == REQ_CODE_PHONE_STATE) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - if (permissionDialog != null && permissionDialog.isShowing()) permissionDialog.dismiss(); - updateCapabilities(); // Fetch capabilities immediately upon grant - } else { - showPermissionBottomSheet(); - } + if (permissionManager != null) { + permissionManager.handleRequestPermissionsResult(requestCode, grantResults); } } @@ -632,18 +374,15 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in protected void onDestroy() { activityDestroyed = true; if (appPreferences != null) appPreferences.unregisterListener(this); - Shizuku.removeBinderReceivedListener(binderReceivedListener); - Shizuku.removeBinderDeadListener(binderDeadListener); - Shizuku.removeRequestPermissionResultListener(permissionResultListener); - - if (rootCheckProcess != null) { - rootCheckProcess.destroy(); - rootCheckProcess = null; + + if (executionStateController != null) { + executionStateController.destroy(); } - if (rootCheckThread != null && rootCheckThread.isAlive()) { - rootCheckThread.interrupt(); - rootCheckThread = null; + + if (permissionManager != null) { + permissionManager.destroy(); } + super.onDestroy(); } @@ -654,75 +393,6 @@ public void onSharedPreferenceChanged(android.content.SharedPreferences sharedPr } } - private void checkRootPermission() { - setStatus("Checking root permission...", 0xFFFFB300); - rootCheckThread = new Thread(() -> { - boolean granted = false; - Process process = null; - try { - process = Runtime.getRuntime().exec(new String[]{"su", "-c", "id"}); - rootCheckProcess = process; - granted = process.waitFor() == 0; - } catch (Exception ignored) { - granted = false; - } finally { - if (process != null) process.destroy(); - if (rootCheckProcess == process) rootCheckProcess = null; - } - - boolean finalGranted = granted; - runOnUiThread(() -> { - if (activityDestroyed || appPreferences == null - || appPreferences.getExecutionMode() != ExecutionMode.ROOT) return; - if (finalGranted) { - setStatus("Root mode active & authorized!", 0xFF1B873F); - appPreferences.setTileErrorState(com.dhangofa.networktoggle.config.AppPreferences.TILE_ERROR_NONE); - } else { - setStatus("Root permission denied or unavailable.", 0xFFFF5555); - appPreferences.setTileErrorState(com.dhangofa.networktoggle.config.AppPreferences.TILE_ERROR_ROOT); - } - android.service.quicksettings.TileService.requestListeningState(MainActivity.this, new android.content.ComponentName(MainActivity.this, NetworkTileService.class)); - }); - }); - rootCheckThread.start(); - } - - private void checkShizukuPermission(boolean requestIfNeeded) { - if (activityDestroyed) return; - try { - if (!Shizuku.pingBinder()) { - setStatus("Shizuku is not running.", 0xFFFF5555); - if (appPreferences != null) { - appPreferences.setTileErrorState(com.dhangofa.networktoggle.config.AppPreferences.TILE_ERROR_SHIZUKU); - android.service.quicksettings.TileService.requestListeningState(MainActivity.this, new android.content.ComponentName(MainActivity.this, NetworkTileService.class)); - } - return; - } - if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { - setStatus("Shizuku mode active & authorized!", 0xFF1B873F); - if (appPreferences != null) { - appPreferences.setTileErrorState(com.dhangofa.networktoggle.config.AppPreferences.TILE_ERROR_NONE); - android.service.quicksettings.TileService.requestListeningState(MainActivity.this, new android.content.ComponentName(MainActivity.this, NetworkTileService.class)); - } - return; - } - setStatus("Shizuku permission not granted.", 0xFFFFB300); - if (appPreferences != null) { - appPreferences.setTileErrorState(com.dhangofa.networktoggle.config.AppPreferences.TILE_ERROR_SHIZUKU); - android.service.quicksettings.TileService.requestListeningState(MainActivity.this, new android.content.ComponentName(MainActivity.this, NetworkTileService.class)); - } - if (requestIfNeeded) { - setStatus("Requesting Shizuku permission...", 0xFFFFB300); - Shizuku.requestPermission(0); - } - } catch (Exception e) { - setStatus("Shizuku check failed.", 0xFFFF5555); - if (appPreferences != null) { - appPreferences.setTileErrorState(com.dhangofa.networktoggle.config.AppPreferences.TILE_ERROR_SHIZUKU); - android.service.quicksettings.TileService.requestListeningState(MainActivity.this, new android.content.ComponentName(MainActivity.this, NetworkTileService.class)); - } - } - } private void setStatus(String text, int color) { statusText.setText(text); @@ -751,34 +421,12 @@ private void setStatus(String text, int color) { - private void updateCarouselContext(boolean authorized) { - if (carouselBackground == null || carouselItems == null) return; - - boolean animate = !isFirstCarouselRender; - isFirstCarouselRender = false; - - if (!authorized) { - if (currentCarouselIndex != 0 || !animate) { - currentCarouselIndex = 0; - renderCarouselPage(0, animate, -1); - } - } else { - // Always start at 2nd page (index 1) on auth transition - if (currentCarouselIndex != 1 || !animate) { - currentCarouselIndex = 1; - renderCarouselPage(1, animate, 1); - } - } - } - private boolean isUIAuthorized = false; - private boolean isFirstCarouselRender = true; - private void updateAuthorizationUI(boolean authorized) { if (isUIAuthorized == authorized) return; isUIAuthorized = authorized; - updateCarouselContext(authorized); + if (carouselManager != null) carouselManager.updateCarouselContext(authorized); float alpha = authorized ? 1.0f : 0.4f; View targetSimCard = findViewById(R.id.targetSimRadioGroup); From b7c1e4691840ef5dbc9255286d1151a93069acc0 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:58:35 +0530 Subject: [PATCH 025/394] Add documentation for CommandResult class Added a comment block to describe the CommandResult class. --- .../java/com/dhangofa/networktoggle/model/CommandResult.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/model/CommandResult.java b/app/src/main/java/com/dhangofa/networktoggle/model/CommandResult.java index 1997515..0532aea 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/model/CommandResult.java +++ b/app/src/main/java/com/dhangofa/networktoggle/model/CommandResult.java @@ -1,5 +1,10 @@ package com.dhangofa.networktoggle.model; +/** + * Wrapper to pass back the results of shell commands (stdout, stderr, exit code). + * Helps easily check if a command succeeded or failed. + */ + public final class CommandResult { private final String command; private final int exitCode; From c5b3a9103517ed24654a4a542a7b2a51abe91881 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:59:33 +0530 Subject: [PATCH 026/394] Document DiagnosticError class functionality Added documentation to the DiagnosticError class to explain its purpose. --- .../com/dhangofa/networktoggle/model/DiagnosticError.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/model/DiagnosticError.java b/app/src/main/java/com/dhangofa/networktoggle/model/DiagnosticError.java index f040ac8..71c66ec 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/model/DiagnosticError.java +++ b/app/src/main/java/com/dhangofa/networktoggle/model/DiagnosticError.java @@ -1,5 +1,10 @@ package com.dhangofa.networktoggle.model; +/** + * Tracks diagnostic errors (like Shizuku Binder death, or Root permission denied) + * to display meaningful error banners in the app. + */ + public class DiagnosticError { public final String command; public final int exitCode; From f1275e02b65dc9ca726c915bdccd7ec760dc98ff Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:00:24 +0530 Subject: [PATCH 027/394] Add JavaDoc for ExecutionMode enum Added documentation for ExecutionMode enum. --- .../java/com/dhangofa/networktoggle/model/ExecutionMode.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/model/ExecutionMode.java b/app/src/main/java/com/dhangofa/networktoggle/model/ExecutionMode.java index 3270e68..72b31f7 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/model/ExecutionMode.java +++ b/app/src/main/java/com/dhangofa/networktoggle/model/ExecutionMode.java @@ -1,5 +1,9 @@ package com.dhangofa.networktoggle.model; +/** + * Enum representing how the app to execute commands (Root, Shizuku, or None/Not setup yet). + */ + public enum ExecutionMode { NONE(0), ROOT(1), From f33f338a7da0f1a18209b3fd7bc833d8fb4aab33 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:01:14 +0530 Subject: [PATCH 028/394] Add documentation for NetworkMode enum Added documentation for NetworkMode enum to clarify its purpose and functionality. --- .../java/com/dhangofa/networktoggle/model/NetworkMode.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/model/NetworkMode.java b/app/src/main/java/com/dhangofa/networktoggle/model/NetworkMode.java index 42573f3..9f8ef33 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/model/NetworkMode.java +++ b/app/src/main/java/com/dhangofa/networktoggle/model/NetworkMode.java @@ -1,5 +1,10 @@ package com.dhangofa.networktoggle.model; +/** + * This enum maps the different network types (like 5G, 4G, 3G) to their internal Android telephony IDs. + * Translates requested modes into the raw values the `cmd phone` commands expect. + */ + public enum NetworkMode { UNKNOWN(0, "Unknown", "Tap to Set 4G", "?", null, -1), FOUR_G_ONLY(1, "4G Only", "4G Only", "4G", "1000000000000", 11), From 8fc13a352cecfd74512965701b901416db3a6fb6 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:01:51 +0530 Subject: [PATCH 029/394] Add Javadoc comment for TargetSim enum Added documentation for TargetSim enum. --- .../main/java/com/dhangofa/networktoggle/model/TargetSim.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/model/TargetSim.java b/app/src/main/java/com/dhangofa/networktoggle/model/TargetSim.java index cfdfffa..6479af1 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/model/TargetSim.java +++ b/app/src/main/java/com/dhangofa/networktoggle/model/TargetSim.java @@ -1,5 +1,9 @@ package com.dhangofa.networktoggle.model; +/** + * Enum representing which SIM slot the app should target (SIM 1, SIM 2, or Auto). + */ + public enum TargetSim { AUTO(0, -1), SIM_1(1, 0), From 54258d9a92bb061b3d69743b5c0190f1bf7ecbf7 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:04:19 +0530 Subject: [PATCH 030/394] Refactor NetworkTileService to use TileIconManager - Replaced hardcoded icon logic by delegating to the new TileIconManager. - Updated the click handler to route network changes through the newly modularized telephony controllers. --- .../networktoggle/NetworkTileService.java | 109 ++++++------------ 1 file changed, 33 insertions(+), 76 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java index ddc8a28..b2eb0a3 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java +++ b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java @@ -1,4 +1,12 @@ package com.dhangofa.networktoggle; + +/** + * Quick Settings (QS) Tile Service. + * This handles the actual toggle button that sits in the Android notification shade. + * When tapped, it reads the current network mode, figures out the next mode based on the configured cycle, + * and executes the change using the chosen backend (Root/Shizuku). + * It also dynamically draws the tile icon to reflect the currently active mode. + */ import android.os.Build; import android.graphics.Bitmap; @@ -30,20 +38,14 @@ import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; +import com.dhangofa.networktoggle.ui.TileIconManager; + public class NetworkTileService extends TileService { private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(); private static final AtomicBoolean IS_SWITCHING = new AtomicBoolean(false); - private static Icon icon4g; - private static Icon icon5g; - private static Icon iconP5g; - private static Icon iconP4g; - private static Icon iconP3g; - private static Icon icon2g; - private static Icon iconUnknown; - private final Handler mainHandler = new Handler(Looper.getMainLooper()); private AppPreferences appPreferences; @@ -88,8 +90,19 @@ public void onStartListening() { NetworkMode cachedMode = appPreferences.getCachedNetworkMode(); updateTileUI(cachedMode); - if (appPreferences.getExecutionMode() == ExecutionMode.NONE - || cachedMode != NetworkMode.UNKNOWN) { + if (appPreferences.getExecutionMode() == ExecutionMode.NONE) { + return; + } + + boolean shouldRefresh = (cachedMode == NetworkMode.UNKNOWN); + long lastCheck = appPreferences.getLastNetworkCheckTimestamp(); + + // 5 minute micro-cooldown before passively re-checking modem + if (!shouldRefresh && (System.currentTimeMillis() - lastCheck > 5 * 60 * 1000L)) { + shouldRefresh = true; + } + + if (!shouldRefresh) { return; } @@ -101,13 +114,14 @@ public void onStartListening() { * A tile click or another operation may have updated the state * while this asynchronous readback was running. */ - if (appPreferences.getCachedNetworkMode() - != NetworkMode.UNKNOWN) { + long newCheck = appPreferences.getLastNetworkCheckTimestamp(); + if (newCheck > lastCheck && cachedMode != NetworkMode.UNKNOWN) { return; } if (realMode != NetworkMode.UNKNOWN) { appPreferences.setCachedNetworkMode(realMode); + appPreferences.setLastNetworkCheckTimestamp(System.currentTimeMillis()); updateTileUI(realMode); } }); @@ -153,6 +167,7 @@ public void onClick() { if (result.isSuccess()) { appPreferences.setCachedNetworkMode(nextMode); + appPreferences.setLastNetworkCheckTimestamp(System.currentTimeMillis()); appPreferences.setAutoSimError(false); appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_NONE); mainHandler.post(() -> { @@ -202,71 +217,13 @@ public void onClick() { }); } - private Icon createTextOnlyIcon(String text) { - int size = 256; - Bitmap bitmap = Bitmap.createBitmap( - size, - size, - Bitmap.Config.ARGB_8888 - ); - Canvas canvas = new Canvas(bitmap); - Paint paint = new Paint(); - paint.setAntiAlias(true); - paint.setColor(Color.WHITE); - paint.setTypeface(Typeface.create( - "sans-serif-condensed", - Typeface.BOLD - )); - paint.setTextAlign(Paint.Align.CENTER); - paint.setTextSize(190f); - - float width = paint.measureText(text); - if (width > 240f) { - paint.setTextScaleX(240f / width); - } - - Paint.FontMetrics metrics = paint.getFontMetrics(); - float y = (size / 2f) - (metrics.descent + metrics.ascent) / 2f; - canvas.drawText(text, size / 2f, y, paint); - - return Icon.createWithBitmap(bitmap); - } - - private Icon getCachedIcon(String text) { - switch (text) { - case "4G": - if (icon4g == null) icon4g = createTextOnlyIcon("4G"); - return icon4g; - case "5G": - if (icon5g == null) icon5g = createTextOnlyIcon("5G"); - return icon5g; - case "P5G": - if (iconP5g == null) iconP5g = createTextOnlyIcon("P5G"); - return iconP5g; - case "P4G": - if (iconP4g == null) iconP4g = createTextOnlyIcon("P4G"); - return iconP4g; - case "P3G": - if (iconP3g == null) iconP3g = createTextOnlyIcon("P3G"); - return iconP3g; - case "2G": - if (icon2g == null) icon2g = createTextOnlyIcon("2G"); - return icon2g; - default: - if (iconUnknown == null) { - iconUnknown = createTextOnlyIcon("?"); - } - return iconUnknown; - } - } - private void updateTileSwitchingUI() { Tile tile = getQsTile(); if (tile == null) return; tile.setState(Tile.STATE_INACTIVE); tile.setLabel("Switching..."); - tile.setIcon(getCachedIcon("?")); + tile.setIcon(TileIconManager.getCachedIcon("?")); tile.updateTile(); } @@ -281,19 +238,19 @@ private void updateTileUI(NetworkMode mode) { if (errorState == AppPreferences.TILE_ERROR_SHIZUKU) { tile.setState(Tile.STATE_UNAVAILABLE); tile.setLabel("Shizuku Unavailable"); - tile.setIcon(getCachedIcon("?")); + tile.setIcon(TileIconManager.getCachedIcon("?")); tile.updateTile(); return; } else if (errorState == AppPreferences.TILE_ERROR_ROOT) { tile.setState(Tile.STATE_UNAVAILABLE); tile.setLabel("Root Unavailable"); - tile.setIcon(getCachedIcon("?")); + tile.setIcon(TileIconManager.getCachedIcon("?")); tile.updateTile(); return; } else if (errorState == AppPreferences.TILE_ERROR_CMD) { tile.setState(Tile.STATE_INACTIVE); tile.setLabel("Error Check App"); - tile.setIcon(getCachedIcon("?")); + tile.setIcon(TileIconManager.getCachedIcon("?")); tile.updateTile(); return; } @@ -309,11 +266,11 @@ private void updateTileUI(NetworkMode mode) { tile.setLabel("Tap to Set " + firstMode.getTileLabel()); } - tile.setIcon(getCachedIcon("?")); + tile.setIcon(TileIconManager.getCachedIcon("?")); } else { tile.setState(Tile.STATE_ACTIVE); tile.setLabel(mode.getTileLabel()); - tile.setIcon(getCachedIcon(mode.getIconText())); + tile.setIcon(TileIconManager.getCachedIcon(mode.getIconText())); } tile.updateTile(); From ea6650fcbe4ff6206050a77d73df425c5189c047 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:06:32 +0530 Subject: [PATCH 031/394] Add class documentation for LteAndAboveCarrierRegistry Added documentation to clarify the purpose of the LteAndAboveCarrierRegistry class. --- .../networktoggle/telephony/LteAndAboveCarrierRegistry.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/LteAndAboveCarrierRegistry.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/LteAndAboveCarrierRegistry.java index add4fa9..732ef14 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/LteAndAboveCarrierRegistry.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/LteAndAboveCarrierRegistry.java @@ -1,5 +1,10 @@ package com.dhangofa.networktoggle.telephony; +/** + * A registry of carriers that require LTE as a minimum. + * Prevents users from dropping to 3G/2G on networks that no longer support it (like Verizon). + */ + import java.util.Arrays; import java.util.List; From ae480bc3599fcee8ff8c8dfb234f3bf958d2b9d2 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:07:35 +0530 Subject: [PATCH 032/394] Add class documentation for NetworkCapabilityResolver Added documentation to clarify the purpose of the NetworkCapabilityResolver class. --- .../networktoggle/telephony/NetworkCapabilityResolver.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkCapabilityResolver.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkCapabilityResolver.java index 3de893b..07200a7 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkCapabilityResolver.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkCapabilityResolver.java @@ -1,5 +1,10 @@ package com.dhangofa.networktoggle.telephony; +/** + * Resolves what network types (5G, 4G, etc.) the phone actually supports. + * It parses raw baseband properties to decide which toggle options should be available in the UI. + */ + import android.os.Build; import com.dhangofa.networktoggle.command.CommandExecutor; From 41eb550e54b8665e2dc543223248aa1a70ba9b4b Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:09:49 +0530 Subject: [PATCH 033/394] Enhance NetworkModeController with Shizuku and Modern methods - Stripped actual shell execution out of this class entirely. - Refactored it to act purely as a routing facade pointing to the new ModernRoot, LegacyRoot, and ShizukuBinder controllers. --- .../telephony/NetworkModeController.java | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeController.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeController.java index 52bb844..0606f80 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeController.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeController.java @@ -1,47 +1,40 @@ package com.dhangofa.networktoggle.telephony; -import android.os.Build; +/** + * Main entry point for changing network modes. + * It figures out whether to use Root or Shizuku, and whether to use the Modern or Legacy root methods + * based on the device API level. + */ -import com.dhangofa.networktoggle.command.CommandExecutor; -import com.dhangofa.networktoggle.command.CommandExecutorFactory; +import android.os.Build; import com.dhangofa.networktoggle.model.CommandResult; import com.dhangofa.networktoggle.model.ExecutionMode; import com.dhangofa.networktoggle.model.NetworkMode; public final class NetworkModeController { - private final SimResolver simResolver; - private final LegacyNetworkModeController legacyController; + private final ShizukuBinderModeController shizukuBinderController; + private final LegacyRootModeController legacyController; + private final ModernRootModeController modernRootController; public NetworkModeController(SimResolver simResolver) { - this.simResolver = simResolver; - this.legacyController = new LegacyNetworkModeController( + this.shizukuBinderController = new ShizukuBinderModeController(simResolver); + this.legacyController = new LegacyRootModeController( simResolver.getContext(), simResolver); + this.modernRootController = new ModernRootModeController(simResolver); } public CommandResult apply(NetworkMode networkMode, ExecutionMode executionMode) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - return legacyController.apply(networkMode, executionMode); - } - - if (networkMode == null || networkMode == NetworkMode.UNKNOWN - || networkMode.getBinaryMask() == null) { - return CommandResult.failed("", "Invalid network mode selected."); + // 1. Shizuku Fast-Path (Binder IPC) + if (executionMode == ExecutionMode.SHIZUKU) { + return shizukuBinderController.apply(networkMode, executionMode); } - int slotIndex = simResolver.resolveTargetSlotIndex(executionMode); - if (!simResolver.isValidSlotIndex(slotIndex)) { - return CommandResult.failed("", - "Unable to resolve the target physical SIM slot."); - } - - String command = "cmd phone set-allowed-network-types-for-users -s " - + slotIndex + " " + networkMode.getBinaryMask(); - - CommandExecutor executor = CommandExecutorFactory.forMode(executionMode); - if (executor == null) { - return CommandResult.failed(command, "No execution mode selected."); + // 2. Root Legacy Fallback (Android 11 and below) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + return legacyController.apply(networkMode, executionMode); } - return executor.execute(command); + // 3. Root Modern Fallback (Android 12+) + return modernRootController.apply(networkMode, executionMode); } } From 048ad17c09a5fa02cc060e743c618abea0e5b084 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:11:18 +0530 Subject: [PATCH 034/394] Refactor NetworkModeReader for improved clarity - Removed raw shell read commands. - Configured the class to delegate reads directly to ShizukuBinderModeReader or RootModeReader based on user settings. --- .../telephony/NetworkModeReader.java | 78 ++++--------------- 1 file changed, 16 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java index 1897832..4d868f9 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java @@ -1,28 +1,28 @@ package com.dhangofa.networktoggle.telephony; -import com.dhangofa.networktoggle.command.CommandExecutor; -import com.dhangofa.networktoggle.command.CommandExecutorFactory; +/** + * Main entry point for reading the current active network mode. + * It routes the request to either the Root or Shizuku readers depending on user settings. + */ + 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 android.content.Context; -import android.provider.Settings; public final class NetworkModeReader { - private final Context context; private final AppPreferences appPreferences; - private final SimResolver simResolver; + private final ShizukuBinderModeReader shizukuBinderReader; + private final RootModeReader rootModeReader; public NetworkModeReader( Context context, AppPreferences appPreferences, SimResolver simResolver ) { - this.context = context; this.appPreferences = appPreferences; - this.simResolver = simResolver; + this.shizukuBinderReader = new ShizukuBinderModeReader(simResolver); + this.rootModeReader = new RootModeReader(context, simResolver); } public NetworkMode readCurrentMode() { @@ -31,61 +31,15 @@ public NetworkMode readCurrentMode() { return NetworkMode.UNKNOWN; } - TargetSim targetSim = appPreferences.getTargetSim(); - int targetSubId = simResolver.resolveTargetSubId(executionMode); - - // NATIVE API FAST-PATH: - // Try reading natively without spawning shell if we have a valid SubId - if (simResolver.isValidSubId(targetSubId)) { - try { - String nativeValue = Settings.Global.getString( - context.getContentResolver(), - "preferred_network_mode" + targetSubId - ); - - if (nativeValue != null && !nativeValue.trim().isEmpty() && !nativeValue.equalsIgnoreCase("null")) { - return NetworkMode.fromLegacyMode(ShellValueParser.extractFirstInt(nativeValue)); - } - } catch (Exception ignored) { - // Ignore SecurityExceptions or null pointers, proceed to shell fallback + // 1. Shizuku Fast-Path (Binder IPC) + if (executionMode == ExecutionMode.SHIZUKU) { + NetworkMode shizukuMode = shizukuBinderReader.readCurrentMode(executionMode); + if (shizukuMode != NetworkMode.UNKNOWN) { + return shizukuMode; } } - // SHELL FALLBACK: - String command; - if (simResolver.isValidSubId(targetSubId)) { - command = "value=$(settings get global preferred_network_mode" - + targetSubId - + "); if [ -n \"$value\" ] " - + "&& [ \"$value\" != \"null\" ]; then " - + "echo \"$value\"; else exit 1; fi"; - } else if (targetSim == TargetSim.AUTO) { - // Very slow nested shell fallback if native SubId resolution entirely failed - 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}"; - } else { - return NetworkMode.UNKNOWN; - } - - CommandResult result = execute(executionMode, command); - if (!result.isSuccess()) { - return NetworkMode.UNKNOWN; - } - - return NetworkMode.fromLegacyMode( - ShellValueParser.extractFirstInt(result.getStdout()) - ); - } - - private CommandResult execute( - ExecutionMode executionMode, - String command - ) { - CommandExecutor executor = CommandExecutorFactory.forMode(executionMode); - if (executor == null) { - return CommandResult.failed(command, "No execution mode selected."); - } - return executor.execute(command); + // 2. Root/Native Fallback Path + return rootModeReader.readCurrentMode(executionMode, appPreferences.getTargetSim()); } } From 6b1d9178545f1bf8a9c57874ad09a3d79dca43c7 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:12:06 +0530 Subject: [PATCH 035/394] Add Javadoc comments to ShellValueParser Added documentation for ShellValueParser class. --- .../dhangofa/networktoggle/telephony/ShellValueParser.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/ShellValueParser.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShellValueParser.java index 296a6c9..8fc27c1 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/ShellValueParser.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShellValueParser.java @@ -1,5 +1,10 @@ package com.dhangofa.networktoggle.telephony; +/** + * Utility to parse the raw text output from the `cmd phone` shell commands + * back into the NetworkMode enum. + */ + import java.util.regex.Matcher; import java.util.regex.Pattern; From dd87072ea538cf8b82e615d1efe0e20bd40d8f92 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:12:56 +0530 Subject: [PATCH 036/394] Add documentation for SimResolver class Added a comment block to explain the purpose of the SimResolver class. --- .../com/dhangofa/networktoggle/telephony/SimResolver.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java index c138086..b46a2d2 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java @@ -1,5 +1,11 @@ package com.dhangofa.networktoggle.telephony; +/** + * Helper to manage multi-SIM devices. + * It handles the logic for resolving "Auto" SIM to the active data slot, and finding the correct + * subId (subscription ID) and phoneId for a given physical SIM slot. + */ + import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; From cfd1f64327efc944f8509b13290080552bb471c7 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:13:39 +0530 Subject: [PATCH 037/394] Add class documentation for TileCycleUiController Added documentation for the TileCycleUiController class. --- .../com/dhangofa/networktoggle/ui/TileCycleUiController.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java b/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java index f16c70b..4b7ccb1 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java @@ -1,5 +1,10 @@ package com.dhangofa.networktoggle.ui; +/** + * UI controller to render and manage the drag-and-drop network cycle list in the app. + * It handles adding, removing, and reordering the network modes that the QS tile will cycle through. + */ + import android.app.Activity; import android.view.View; import android.widget.CheckBox; From 91ceeae3755a8e660697d7d7d7eec2372dbb8b4f Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:15:44 +0530 Subject: [PATCH 038/394] Add Javadoc comments to DiagnosticReporter class Added documentation for DiagnosticReporter utility class. --- .../com/dhangofa/networktoggle/util/DiagnosticReporter.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/util/DiagnosticReporter.java b/app/src/main/java/com/dhangofa/networktoggle/util/DiagnosticReporter.java index ad57e4a..f3c0581 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/util/DiagnosticReporter.java +++ b/app/src/main/java/com/dhangofa/networktoggle/util/DiagnosticReporter.java @@ -1,5 +1,11 @@ package com.dhangofa.networktoggle.util; +/** + * Utility class to generate detailed crash logs and diagnostic strings. + * It grabs device info, Android versions, Shizuku state, and network capabilities so to easily + * debug issues when things go wrong. + */ + import android.os.Build; import com.dhangofa.networktoggle.config.AppPreferences; import com.dhangofa.networktoggle.model.DiagnosticError; From 09f00f57241389703ce9e93c7593ade4298ddf43 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:35:47 +0530 Subject: [PATCH 039/394] Add PrivilegedModeReader class - Created to replace the legacy RootModeReader for better semantic accuracy. - Added a fast-path to check native Settings.Global APIs before falling back to Root or Shizuku privileged shell execution. - Extracted logic to improve testability and maintain modular architecture. --- ...eReader.java => PrivilegedModeReader.java} | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) rename app/src/main/java/com/dhangofa/networktoggle/telephony/{RootModeReader.java => PrivilegedModeReader.java} (72%) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/RootModeReader.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/PrivilegedModeReader.java similarity index 72% rename from app/src/main/java/com/dhangofa/networktoggle/telephony/RootModeReader.java rename to app/src/main/java/com/dhangofa/networktoggle/telephony/PrivilegedModeReader.java index 372ac1c..b1b793c 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/RootModeReader.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/PrivilegedModeReader.java @@ -1,10 +1,9 @@ package com.dhangofa.networktoggle.telephony; /** - * Parses the current network mode by executing root shell commands. - * It uses standard shell parsing to read the active mode out of the telephony service. + * Parses the current network mode through native Settings where supported, + * then the selected Root or Shizuku privileged shell fallback. */ - import android.content.Context; import android.os.Build; import android.provider.Settings; @@ -16,24 +15,25 @@ import com.dhangofa.networktoggle.model.NetworkMode; import com.dhangofa.networktoggle.model.TargetSim; -final class RootModeReader { +final class PrivilegedModeReader { + private final Context context; private final SimResolver simResolver; - RootModeReader(Context context, SimResolver simResolver) { + PrivilegedModeReader(Context context, SimResolver simResolver) { this.context = context; this.simResolver = simResolver; } NetworkMode readCurrentMode(ExecutionMode executionMode, TargetSim targetSim) { int targetSubId = simResolver.resolveTargetSubId(executionMode); - + // NATIVE API FAST-PATH: // Only works reliably on Android 9 (Pie) and below. Android 10+ will throw SecurityException. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && simResolver.isValidSubId(targetSubId)) { try { String nativeValue = Settings.Global.getString( - context.getContentResolver(), + context.getContentResolver(), "preferred_network_mode" + targetSubId ); @@ -44,36 +44,31 @@ NetworkMode readCurrentMode(ExecutionMode executionMode, TargetSim targetSim) { // Ignore SecurityExceptions or null pointers, proceed to shell fallback } } - + // SHELL FALLBACK: String command; if (simResolver.isValidSubId(targetSubId)) { - command = "value=$(settings get global preferred_network_mode" - + targetSubId - + "); if [ -n \"$value\" ] " - + "&& [ \"$value\" != \"null\" ]; then " - + "echo \"$value\"; else exit 1; fi"; + command = "value=$(settings get global preferred_network_mode" + targetSubId + "); " + + "if [ -n \"$value\" ] && [ \"$value\" != \"null\" ]; then echo \"$value\"; else exit 1; fi"; } else if (targetSim == TargetSim.AUTO) { // Very slow nested shell fallback if native SubId resolution entirely failed - 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}"; + 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}"; } else { return NetworkMode.UNKNOWN; } - + CommandExecutor executor = CommandExecutorFactory.forMode(executionMode); + if (executor == null) { return NetworkMode.UNKNOWN; } CommandResult result = executor.execute(command); - if (!result.isSuccess()) { - return NetworkMode.UNKNOWN; - } - - return NetworkMode.fromLegacyMode( - ShellValueParser.extractFirstInt(result.getStdout()) - ); + + return result.isSuccess() + ? NetworkMode.fromLegacyMode(ShellValueParser.extractFirstInt(result.getStdout())) + : NetworkMode.UNKNOWN; } } From dcfa20ed40a1cf916cd4824e27bef92cfab604ef Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:45:57 +0530 Subject: [PATCH 040/394] Add TelephonyMethodHelper for robust reflection This utility class provides methods to safely find hidden Android API methods and unwrap reflection exceptions for better error diagnostics. - Introduced a reflection helper to gracefully iterate through varying method signatures. - Solved compatibility issues with OEMs (Samsung, Xiaomi, OnePlus) that customize hidden ITelephony APIs. - Encapsulated exception handling to generate cleaner diagnostic error messages. --- .../telephony/TelephonyMethodHelper.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/telephony/TelephonyMethodHelper.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/TelephonyMethodHelper.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/TelephonyMethodHelper.java new file mode 100644 index 0000000..1519053 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/TelephonyMethodHelper.java @@ -0,0 +1,74 @@ +package com.dhangofa.networktoggle.telephony; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Utility class designed to resolve hidden Android API fragmentation across different device OEMs. + * It provides safe reflection methods to locate varying ITelephony method signatures and + * unwrap nested reflection exceptions for clearer diagnostic logging. + */ +final class TelephonyMethodHelper { + private TelephonyMethodHelper() {} + + /** + * Attempts to find a method within a given class by iterating through a prioritized list + * of potential method signatures. This gracefully handles OEM-specific framework modifications + * (e.g., Samsung or Xiaomi appending extra parameters like a calling package string). + * + * @param type The class to inspect (e.g., ITelephony.class). + * @param name The exact string name of the target method. + * @param signatures A varargs array of potential parameter type arrays to check, in order of preference. + * @return The matching Method object, or null if no compatible signature is found. + */ + static Method find(Class type, String name, Class[]... signatures) { + for (Class[] signature : signatures) { + for (Method method : type.getDeclaredMethods()) { + if (!name.equals(method.getName())) { + continue; + } + + Class[] actual = method.getParameterTypes(); + if (actual.length != signature.length) { + continue; + } + + boolean match = true; + for (int i = 0; i < actual.length; i++) { + if (actual[i] != signature[i]) { + match = false; + break; + } + } + + if (match) { + return method; + } + } + } + return null; + } + + /** + * Unwraps reflection exceptions to extract the true underlying cause. + * Reflection failures often bury the actual error (like a SecurityException) inside an + * InvocationTargetException. This method drills down to the root Throwable to generate + * readable error messages for diagnostics. + * + * @param throwable The exception caught during reflection. + * @return A clean string describing the root cause class name and message. + */ + static String describe(Throwable throwable) { + Throwable cause = throwable; + while (cause instanceof InvocationTargetException && ((InvocationTargetException) cause).getTargetException() != null) { + cause = ((InvocationTargetException) cause).getTargetException(); + } + + String message = cause.getMessage(); + if (message == null || message.trim().isEmpty()) { + return cause.getClass().getName(); + } else { + return cause.getClass().getName() + ": " + message; + } + } +} From d5ae42518d0ee8c47785222c14d138f86b85e54b Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:48:37 +0530 Subject: [PATCH 041/394] Implement TargetSimUiController for SIM selection This class manages the UI for selecting and validating target SIM options, including handling user interactions and displaying warnings. - Extracted Target SIM UI setup and management from the monolithic MainActivity. - Centralized the logic for handling radio button selections, unsupported slot warnings, and UI separators. - Handled UI dimming and touch interception when execution modes are unauthorized. --- .../ui/TargetSimUiController.java | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/ui/TargetSimUiController.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/TargetSimUiController.java b/app/src/main/java/com/dhangofa/networktoggle/ui/TargetSimUiController.java new file mode 100644 index 0000000..d93527d --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/TargetSimUiController.java @@ -0,0 +1,176 @@ +package com.dhangofa.networktoggle.ui; + +/** Handles Target SIM selection, validation, separators, and the Auto SIM warning. */ +import android.Manifest; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.view.MotionEvent; +import android.view.View; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.Toast; + +import com.dhangofa.networktoggle.R; +import com.dhangofa.networktoggle.config.AppPreferences; +import com.dhangofa.networktoggle.model.TargetSim; + +import java.util.List; + +public final class TargetSimUiController { + private final Activity activity; + private final AppPreferences prefs; + private final Runnable changed; + + private final RadioGroup group; + private final RadioButton auto; + private final RadioButton sim1; + private final RadioButton sim2; + private final TextView warning; + private final View sep1; + private final View sep2; + + private boolean updating; + private boolean authorized; + + public TargetSimUiController(Activity activity, AppPreferences prefs, Runnable changed) { + this.activity = activity; + this.prefs = prefs; + this.changed = changed; + + this.group = activity.findViewById(R.id.targetSimRadioGroup); + this.auto = activity.findViewById(R.id.radioSimAuto); + this.sim1 = activity.findViewById(R.id.radioSim1); + this.sim2 = activity.findViewById(R.id.radioSim2); + this.warning = activity.findViewById(R.id.autoSimWarningText); + this.sep1 = activity.findViewById(R.id.separatorAutoSim1); + this.sep2 = activity.findViewById(R.id.separatorSim1Sim2); + } + + public void initialize() { + TargetSim targetSim = prefs.getTargetSim(); + + if (targetSim == TargetSim.SIM_1) { + sim1.setChecked(true); + } else if (targetSim == TargetSim.SIM_2) { + sim2.setChecked(true); + } else { + auto.setChecked(true); + } + + View.OnTouchListener lock = (v, e) -> { + if (!authorized && e.getAction() == MotionEvent.ACTION_DOWN) { + Toast.makeText(activity, "Please authorize Root or Shizuku to configure toggles.", Toast.LENGTH_SHORT).show(); + return true; + } + return false; + }; + + auto.setOnTouchListener(lock); + sim1.setOnTouchListener(lock); + sim2.setOnTouchListener(lock); + + group.setOnCheckedChangeListener((g, id) -> select(id)); + updateSeparators(); + updateAutoSimWarning(); + } + + private void select(int id) { + if (updating) return; + + TargetSim targetSim = TargetSim.AUTO; + if (id == R.id.radioSim1) { + targetSim = TargetSim.SIM_1; + } else if (id == R.id.radioSim2) { + targetSim = TargetSim.SIM_2; + } + + if (targetSim != TargetSim.AUTO && !exists(targetSim)) { + Toast.makeText(activity, "No SIM card found in slot " + (targetSim.getManualSlotIndex() + 1), Toast.LENGTH_SHORT).show(); + updating = true; + auto.setChecked(true); + updating = false; + targetSim = TargetSim.AUTO; + } + + updateSeparators(); + prefs.onTargetSimChanged(targetSim); + updateAutoSimWarning(); + + if (changed != null) { + changed.run(); + } + } + + private boolean exists(TargetSim target) { + if (activity.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) { + return true; + } + + SubscriptionManager sm = activity.getSystemService(SubscriptionManager.class); + if (sm == null) { + return true; + } + + try { + List infos = sm.getActiveSubscriptionInfoList(); + if (infos == null) return false; + + for (SubscriptionInfo info : infos) { + if (info.getSimSlotIndex() == target.getManualSlotIndex()) { + return true; + } + } + } catch (Exception ignored) { + return true; + } + return false; + } + + public void setAuthorized(boolean value) { + this.authorized = value; + float alpha = value ? 1f : 0.4f; + group.setAlpha(alpha); + + View header = activity.findViewById(R.id.targetSimHeaderContainer); + if (header != null) { + header.setAlpha(alpha); + } + if (warning != null) { + warning.setAlpha(alpha); + } + } + + public void updateAutoSimWarning() { + if (warning == null) return; + + boolean show = prefs.hasAutoSimError() && prefs.getTargetSim() == TargetSim.AUTO; + warning.setVisibility(show ? View.VISIBLE : View.GONE); + + if (show) { + warning.setText("Auto SIM detection failed. Please choose SIM 1 or SIM 2 manually."); + warning.setTextColor(0xFFFF5555); + } + } + + private void updateSeparators() { + int id = group.getCheckedRadioButtonId(); + + if (id == -1) { + sep1.setVisibility(View.VISIBLE); + sep2.setVisibility(View.VISIBLE); + } else if (id == R.id.radioSimAuto) { + sep1.setVisibility(View.INVISIBLE); + sep2.setVisibility(View.VISIBLE); + } else if (id == R.id.radioSim1) { + sep1.setVisibility(View.INVISIBLE); + sep2.setVisibility(View.INVISIBLE); + } else { + sep1.setVisibility(View.VISIBLE); + sep2.setVisibility(View.INVISIBLE); + } + } +} + From 53ae592a73d4471d7140836731a009c9cf04268e Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:50:30 +0530 Subject: [PATCH 042/394] Implement TileCycleSyncController for network mode sync - Extracted the background network mode synchronization behavior from MainActivity. - Delegated the actual cycle validation and application logic to a standalone class to adhere to the single-responsibility principle. - Safely routes state changes through the new centralized AppExecutors pool. --- .../ui/TileCycleSyncController.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleSyncController.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleSyncController.java b/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleSyncController.java new file mode 100644 index 0000000..5173695 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleSyncController.java @@ -0,0 +1,60 @@ +package com.dhangofa.networktoggle.ui; + +/** Synchronizes the active modem mode when the Quick Tile Cycle changes. */ +import android.content.Context; +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.telephony.NetworkModeController; +import com.dhangofa.networktoggle.telephony.NetworkModeReader; +import com.dhangofa.networktoggle.telephony.SimResolver; +import com.dhangofa.networktoggle.util.AppExecutors; +import java.util.List; + +public final class TileCycleSyncController implements TileCycleUiController.OnCycleChangedListener { + + private final Context context; + private final AppPreferences prefs; + private final SimResolver simResolver; + private final NetworkModeController controller; + + public TileCycleSyncController(Context context, AppPreferences prefs, SimResolver simResolver, NetworkModeController controller) { + this.context = context.getApplicationContext(); + this.prefs = prefs; + this.simResolver = simResolver; + this.controller = controller; + } + + @Override + public void onCycleChanged(List newCycle) { + AppExecutors.executeTelephony(() -> sync(newCycle)); + } + + private void sync(List newCycle) { + NetworkMode currentMode = prefs.getCachedNetworkMode(); + + // If cache is wiped (e.g. from SIM switch), but execution is allowed, read it directly once + if (currentMode == NetworkMode.UNKNOWN && prefs.getExecutionMode() != ExecutionMode.NONE) { + currentMode = new NetworkModeReader(context, prefs, simResolver).readCurrentMode(); + if (currentMode != NetworkMode.UNKNOWN) { + prefs.setLastNetworkCheckTimestamp(System.currentTimeMillis()); + prefs.setCachedNetworkMode(currentMode); + } + } + + if (currentMode != NetworkMode.UNKNOWN && !newCycle.contains(currentMode)) { + NetworkMode fallbackMode = newCycle.get(0); + + // Attempt to sync the network state silently + CommandResult result = controller.apply(fallbackMode, prefs.getExecutionMode()); + if (result.isSuccess()) { + prefs.setCachedNetworkMode(fallbackMode); + prefs.setLastNetworkCheckTimestamp(System.currentTimeMillis()); + } + } else if (currentMode != NetworkMode.UNKNOWN) { + // It's in the cycle, ensure it is cached so UI shows the actual active state + prefs.setCachedNetworkMode(currentMode); + } + } +} From 25b4e7718dab91485840a78fa45eabfae7d5974c Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:53:18 +0530 Subject: [PATCH 043/394] Add AppExecutors for centralized threading Implement AppExecutors for managing telephony tasks with a dedicated thread pool. - Created a centralized, single-thread ThreadPoolExecutor dedicated to telephony tasks. - Prevented thread bloat and race conditions by forcefully serializing all modem radio requests. - Set a 30-second keep-alive to conserve resources when the app is idle. --- .../networktoggle/util/AppExecutors.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/util/AppExecutors.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/util/AppExecutors.java b/app/src/main/java/com/dhangofa/networktoggle/util/AppExecutors.java new file mode 100644 index 0000000..6039373 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/util/AppExecutors.java @@ -0,0 +1,24 @@ +package com.dhangofa.networktoggle.util; + +/** Central executor for serialized, short-lived telephony work. */ +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public final class AppExecutors { + + private static final ThreadPoolExecutor TELEPHONY = new ThreadPoolExecutor( + 0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), runnable -> { + Thread thread = new Thread(runnable, "NetToggle-telephony"); + thread.setDaemon(true); + return thread; + }); + + private AppExecutors() {} + + public static void executeTelephony(Runnable task) { + if (task != null) { + TELEPHONY.execute(task); + } + } +} From d2838af5159566075c1f925aba50c1afa8f7df2b Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:55:42 +0530 Subject: [PATCH 044/394] MainActivity UI modularization and fix dimming bug - Delegated the remaining UI logic to TargetSimUiController and TileCycleSyncController. - Resolved the UI authorization race condition by moving loadSavedExecutionMode() to the end of the onCreate lifecycle. - Inverted the initial isUIAuthorized state to true to guarantee a proper visual refresh on fresh installs. - Updated the developer FAQ link to accurately point to the GitHub Wiki page. --- .../dhangofa/networktoggle/MainActivity.java | 60 +++++++------------ 1 file changed, 23 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java index 95d6fc7..18088be 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java +++ b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java @@ -39,6 +39,9 @@ import com.dhangofa.networktoggle.telephony.SimResolver; import com.dhangofa.networktoggle.ui.TileCycleUiController; import com.dhangofa.networktoggle.ui.DialogHelper; +import com.dhangofa.networktoggle.ui.TargetSimUiController; +import com.dhangofa.networktoggle.ui.TileCycleSyncController; +import com.dhangofa.networktoggle.util.AppExecutors; import android.app.Dialog; import android.view.Gravity; import android.view.ViewGroup; @@ -90,12 +93,13 @@ public class MainActivity extends Activity implements android.content.SharedPref private TileCycleUiController tileCycleUiController; private volatile boolean activityDestroyed; private com.dhangofa.networktoggle.ui.ExecutionStateController executionStateController; + private TargetSimUiController targetSimUiController; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); activityDestroyed = false; - + // Triggering fresh deploy to streaming emulator to clear cache/state configureStatusBar(); setContentView(R.layout.activity_main); @@ -114,44 +118,23 @@ protected void onCreate(Bundle savedInstanceState) { executionStateController = new com.dhangofa.networktoggle.ui.ExecutionStateController(this, appPreferences, this::setStatus); - tileCycleUiController.setOnCycleChangedListener(newCycle -> { - new Thread(() -> { - NetworkMode currentMode = appPreferences.getCachedNetworkMode(); - - // If cache is wiped (e.g. from SIM switch), but execution is allowed, read it directly once - if (currentMode == NetworkMode.UNKNOWN && appPreferences.getExecutionMode() != ExecutionMode.NONE) { - currentMode = new com.dhangofa.networktoggle.telephony.NetworkModeReader(this, appPreferences, simResolver).readCurrentMode(); - if (currentMode != NetworkMode.UNKNOWN) { - appPreferences.setLastNetworkCheckTimestamp(System.currentTimeMillis()); - appPreferences.setCachedNetworkMode(currentMode); - } - } - - if (currentMode != NetworkMode.UNKNOWN && !newCycle.contains(currentMode)) { - NetworkMode fallbackMode = newCycle.get(0); - // Attempt to sync the network state silently - com.dhangofa.networktoggle.model.CommandResult result = modeController.apply(fallbackMode, appPreferences.getExecutionMode()); - if (result.isSuccess()) { - appPreferences.setCachedNetworkMode(fallbackMode); - appPreferences.setLastNetworkCheckTimestamp(System.currentTimeMillis()); - } - } else if (currentMode != NetworkMode.UNKNOWN) { - // It's in the cycle, ensure it is cached so UI shows the actual active state - appPreferences.setCachedNetworkMode(currentMode); - } - }).start(); - }); + TileCycleSyncController tileCycleSyncController = new TileCycleSyncController( + this, appPreferences, simResolver, modeController); + tileCycleUiController.setOnCycleChangedListener(tileCycleSyncController); tileCycleUiController.initialize(); executionStateController.registerListeners(); - loadSavedExecutionMode(); - loadSavedTargetSimMode(); - updateAutoSimWarning(); + targetSimUiController = new TargetSimUiController( + this, appPreferences, this::updateCapabilities); + targetSimUiController.initialize(); bindSelectionListeners(); updateSeparatorVisibility(); carouselManager = new com.dhangofa.networktoggle.ui.CarouselManager(this); - carouselManager.setupCarousel(isUIAuthorized); + carouselManager.setupCarousel(false); // Default to false, loadSavedExecutionMode will correct it setupStaticLandscapeCards(); + + // Load initial mode and update UI authorization state (triggers dimming if NONE) + loadSavedExecutionMode(); } private void configureStatusBar() { @@ -194,7 +177,7 @@ private void bindViews() { private void bindLinks() { githubLink.setOnClickListener(v -> openUrl("https://github.com/Dhangofa/NetToggle")); telegramLink.setOnClickListener(v -> openUrl("https://t.me/dhangofas_projects_chat")); - if (faqLink != null) faqLink.setOnClickListener(v -> openUrl("https://github.com/Dhangofa/NetToggle/wiki")); + if (faqLink != null) faqLink.setOnClickListener(v -> openUrl("https://github.com/Dhangofa/NetToggle/wiki/Frequently-Asked-Questions-(FAQ)")); } @@ -319,7 +302,7 @@ private void bindSelectionListeners() { protected void onResume() { super.onResume(); - if (appPreferences != null) updateAutoSimWarning(); + if (targetSimUiController != null) targetSimUiController.updateAutoSimWarning(); if (permissionManager != null) permissionManager.checkAndRequest(); updateErrorBanner(); } @@ -352,14 +335,14 @@ private void showDiagnosticDialog() { } private void updateCapabilities() { - new Thread(() -> { + AppExecutors.executeTelephony(() -> { AppPreferences.NetworkCapabilities caps = capabilityResolver.getCapabilities(appPreferences.getExecutionMode()); runOnUiThread(() -> { if (!activityDestroyed && tileCycleUiController != null) { tileCycleUiController.applyCapabilities(caps); } }); - }).start(); + }); } @Override @@ -421,7 +404,7 @@ private void setStatus(String text, int color) { - private boolean isUIAuthorized = false; + private boolean isUIAuthorized = true; private void updateAuthorizationUI(boolean authorized) { if (isUIAuthorized == authorized) return; @@ -439,6 +422,9 @@ private void updateAuthorizationUI(boolean authorized) { if (tileCycleUiController != null) { tileCycleUiController.setAuthorized(authorized); } + if (targetSimUiController != null) { + targetSimUiController.setAuthorized(authorized); + } if (authorized && appPreferences != null) { appPreferences.clearDeviceCapabilities(); From 37c7089f8d604ee27126913ce8e14b390c2eb911 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:57:23 +0530 Subject: [PATCH 045/394] Refactor network tile service to use centralized AppExecutors - Replaced the local Executors.newSingleThreadExecutor() with the globally managed AppExecutors.executeTelephony(). - Ensured Quick Settings tile toggles are serialized alongside in-app UI requests to prevent radio hardware hangs. --- .../com/dhangofa/networktoggle/NetworkTileService.java | 9 +++------ 1 file changed, 3 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 b2eb0a3..5f29ca8 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java +++ b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java @@ -33,16 +33,13 @@ import com.dhangofa.networktoggle.telephony.NetworkModeReader; import com.dhangofa.networktoggle.telephony.SimResolver; import com.dhangofa.networktoggle.cycle.TileCycleManager; +import com.dhangofa.networktoggle.util.AppExecutors; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import com.dhangofa.networktoggle.ui.TileIconManager; public class NetworkTileService extends TileService { - private static final ExecutorService EXECUTOR = - Executors.newSingleThreadExecutor(); private static final AtomicBoolean IS_SWITCHING = new AtomicBoolean(false); @@ -106,7 +103,7 @@ public void onStartListening() { return; } - EXECUTOR.execute(() -> { + AppExecutors.executeTelephony(() -> { NetworkMode realMode = networkModeReader.readCurrentMode(); mainHandler.post(() -> { @@ -148,7 +145,7 @@ public void onClick() { NetworkMode nextMode = tileCycleManager.getNextMode(currentMode); updateTileSwitchingUI(); - EXECUTOR.execute(() -> { + AppExecutors.executeTelephony(() -> { int slotIndex = simResolver.resolveTargetSlotIndex(executionMode); if (!simResolver.isValidSlotIndex(slotIndex)) { mainHandler.post(() -> { From bc0851468267d20fff25654c343afd1a8c386c43 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:59:50 +0530 Subject: [PATCH 046/394] Update NetworkModeReader routing - Updated class dependencies to utilize PrivilegedModeReader instead of RootModeReader. - Maintained its role as a clean facade routing state reads to the correct backend. --- .../telephony/NetworkModeReader.java | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java index 4d868f9..f9b09d8 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java @@ -5,41 +5,32 @@ * It routes the request to either the Root or Shizuku readers depending on user settings. */ +import android.content.Context; import com.dhangofa.networktoggle.config.AppPreferences; import com.dhangofa.networktoggle.model.ExecutionMode; import com.dhangofa.networktoggle.model.NetworkMode; -import android.content.Context; public final class NetworkModeReader { - private final AppPreferences appPreferences; - private final ShizukuBinderModeReader shizukuBinderReader; - private final RootModeReader rootModeReader; - - public NetworkModeReader( - Context context, - AppPreferences appPreferences, - SimResolver simResolver - ) { - this.appPreferences = appPreferences; - this.shizukuBinderReader = new ShizukuBinderModeReader(simResolver); - this.rootModeReader = new RootModeReader(context, simResolver); - } - - public NetworkMode readCurrentMode() { - ExecutionMode executionMode = appPreferences.getExecutionMode(); - if (executionMode == ExecutionMode.NONE) { - return NetworkMode.UNKNOWN; - } - - // 1. Shizuku Fast-Path (Binder IPC) - if (executionMode == ExecutionMode.SHIZUKU) { - NetworkMode shizukuMode = shizukuBinderReader.readCurrentMode(executionMode); - if (shizukuMode != NetworkMode.UNKNOWN) { - return shizukuMode; - } - } - - // 2. Root/Native Fallback Path - return rootModeReader.readCurrentMode(executionMode, appPreferences.getTargetSim()); - } + private final AppPreferences appPreferences; + private final ShizukuBinderModeReader shizukuBinderReader; + private final PrivilegedModeReader privilegedModeReader; + + public NetworkModeReader(Context context, AppPreferences appPreferences, SimResolver simResolver) { + this.appPreferences = appPreferences; + this.shizukuBinderReader = new ShizukuBinderModeReader(simResolver); + this.privilegedModeReader = new PrivilegedModeReader(context, simResolver); + } + + public NetworkMode readCurrentMode() { + ExecutionMode executionMode = appPreferences.getExecutionMode(); + if (executionMode == ExecutionMode.NONE) return NetworkMode.UNKNOWN; + // 1. Shizuku Fast-Path (Binder IPC) + if (executionMode == ExecutionMode.SHIZUKU) { + NetworkMode mode = shizukuBinderReader.readCurrentMode(executionMode); + if (mode != NetworkMode.UNKNOWN) return mode; + } + // 2. Root/Shizuku privileged shell fallback path + return privilegedModeReader.readCurrentMode(executionMode, appPreferences.getTargetSim()); + } } + From ac50836c4c94daa480bd420ecee6c2b40b6206ad Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:01:50 +0530 Subject: [PATCH 047/394] Fortify ShizukuBinderModeController reflection - Refactored ITelephony IPC invocation to use TelephonyMethodHelper. - Added dynamic signature matching for setAllowedNetworkTypesForReason (Android 12+) and setPreferredNetworkType (Android 11-). - Improved Binder ping checks to catch dead Android phone services early. --- .../ShizukuBinderModeController.java | 144 +++++++++--------- 1 file changed, 70 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeController.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeController.java index 4193b9d..076c327 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeController.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeController.java @@ -20,6 +20,8 @@ import rikka.shizuku.SystemServiceHelper; final class ShizukuBinderModeController { + + private static final String PACKAGE = "com.dhangofa.networktoggle"; private final SimResolver simResolver; ShizukuBinderModeController(SimResolver simResolver) { @@ -30,106 +32,100 @@ CommandResult apply(NetworkMode networkMode, ExecutionMode executionMode) { if (executionMode != ExecutionMode.SHIZUKU) { return CommandResult.failed("", "Invalid execution mode for Shizuku binder."); } - + if (networkMode == null || networkMode == NetworkMode.UNKNOWN) { return CommandResult.failed("", "Invalid network mode selected."); } - + SimResolver.SimInfo simInfo = simResolver.resolveTargetSimInfo(executionMode); + if (simInfo == null || !simResolver.isValidSlotIndex(simInfo.slotIndex) || !simResolver.isValidSubId(simInfo.subId)) { return CommandResult.failed("", "Unable to resolve the target SIM."); } - + try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { org.lsposed.hiddenapibypass.HiddenApiBypass.addHiddenApiExemptions("Lcom/android/internal/telephony/"); } - - IBinder binder = new ShizukuBinderWrapper(SystemServiceHelper.getSystemService("phone")); - if (binder == null) { - return CommandResult.failed("", "Failed to get Shizuku 'phone' service binder."); + + IBinder raw = SystemServiceHelper.getSystemService("phone"); + if (raw == null || !raw.pingBinder()) { + return CommandResult.failed("AIDL: phone service", "Android phone service Binder is unavailable."); } - - @SuppressLint("PrivateApi") - Class iTelephonyStubClass = Class.forName("com.android.internal.telephony.ITelephony$Stub"); - Method asInterfaceMethod = iTelephonyStubClass.getDeclaredMethod("asInterface", IBinder.class); - Object iTelephony = asInterfaceMethod.invoke(null, binder); - - if (iTelephony == null) { + + IBinder binder = new ShizukuBinderWrapper(raw); + if (!binder.pingBinder()) { + return CommandResult.failed("AIDL: phone service", "Shizuku phone service Binder is not responding."); + } + + @SuppressLint("PrivateApi") + Class stub = Class.forName("com.android.internal.telephony.ITelephony$Stub"); + Object phone = stub.getDeclaredMethod("asInterface", IBinder.class).invoke(null, binder); + + if (phone == null) { return CommandResult.failed("", "Failed to get ITelephony interface via Shizuku."); } - - Class iTelephonyClass = Class.forName("com.android.internal.telephony.ITelephony"); - + + Class api = Class.forName("com.android.internal.telephony.ITelephony"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // Android 12+ (API 31+) if (networkMode.getBinaryMask() == null) { return CommandResult.failed("", "Invalid network mode mask for A12+."); } - long bitmask = Long.parseLong(networkMode.getBinaryMask(), 2); - Method setAllowedMethod = null; - for (Method m : iTelephonyClass.getDeclaredMethods()) { - if (m.getName().equals("setAllowedNetworkTypesForReason")) { - setAllowedMethod = m; - break; - } - } - - if (setAllowedMethod == null) { - return CommandResult.failed("AIDL: setAllowedNetworkTypesForReason", "Method not found in ITelephony on this OEM device."); - } - - Class[] pTypes = setAllowedMethod.getParameterTypes(); - if (pTypes.length == 3) { - setAllowedMethod.invoke(iTelephony, simInfo.subId, 0, bitmask); - } else if (pTypes.length == 4 && pTypes[3] == String.class) { - setAllowedMethod.invoke(iTelephony, simInfo.subId, 0, bitmask, "com.dhangofa.networktoggle"); - } else if (pTypes.length == 2) { - // Some builds might just take subId and bitmask - setAllowedMethod.invoke(iTelephony, simInfo.subId, bitmask); - } else { - return CommandResult.failed("AIDL: setAllowedNetworkTypesForReason", "Unknown method signature length: " + pTypes.length); - } - - return CommandResult.completed("AIDL: setAllowedNetworkTypesForReason", 0, "Network mode applied successfully via Shizuku Binder.", ""); - } else { - // Android 11- (API <= 30) - if (networkMode.getLegacyMode() < 0) { - return CommandResult.failed("", "Invalid legacy network mode."); - } + long mask = Long.parseLong(networkMode.getBinaryMask(), 2); + Method method = TelephonyMethodHelper.find( + api, + "setAllowedNetworkTypesForReason", + new Class[] {int.class, int.class, long.class}, + new Class[] {int.class, int.class, long.class, String.class}, + new Class[] {int.class, long.class} + ); - Method setPrefMethod = null; - for (Method m : iTelephonyClass.getDeclaredMethods()) { - if (m.getName().equals("setPreferredNetworkType")) { - setPrefMethod = m; - break; - } - } - - if (setPrefMethod == null) { - return CommandResult.failed("AIDL: setPreferredNetworkType", "Method not found in ITelephony on this OEM device."); - } - - Object result = null; - Class[] pTypes = setPrefMethod.getParameterTypes(); - if (pTypes.length == 2) { - result = setPrefMethod.invoke(iTelephony, simInfo.subId, networkMode.getLegacyMode()); - } else if (pTypes.length == 3 && pTypes[2] == String.class) { - result = setPrefMethod.invoke(iTelephony, simInfo.subId, networkMode.getLegacyMode(), "com.dhangofa.networktoggle"); - } else { - return CommandResult.failed("AIDL: setPreferredNetworkType", "Unknown method signature length: " + pTypes.length); + if (method == null) { + return CommandResult.failed("AIDL: setAllowedNetworkTypesForReason", "No compatible method signature found in ITelephony."); } + Object result = method.getParameterCount() == 3 + ? method.invoke(phone, simInfo.subId, 0, mask) + : method.getParameterCount() == 4 + ? method.invoke(phone, simInfo.subId, 0, mask, PACKAGE) + : method.invoke(phone, simInfo.subId, mask); + if (result instanceof Boolean && !((Boolean) result)) { - return CommandResult.failed("AIDL: setPreferredNetworkType", "ITelephony returned false."); + return CommandResult.failed("AIDL: setAllowedNetworkTypesForReason", "ITelephony returned false."); } - return CommandResult.completed("AIDL: setPreferredNetworkType", 0, "Legacy network mode applied successfully via Shizuku Binder.", ""); + return CommandResult.completed("AIDL: setAllowedNetworkTypesForReason", 0, "Network mode applied successfully via Shizuku Binder.", ""); + } + + if (networkMode.getLegacyMode() < 0) { + return CommandResult.failed("", "Invalid legacy network mode."); + } + + Method method = TelephonyMethodHelper.find( + api, + "setPreferredNetworkType", + new Class[] {int.class, int.class}, + new Class[] {int.class, int.class, String.class} + ); + + if (method == null) { + return CommandResult.failed("AIDL: setPreferredNetworkType", "No compatible method signature found in ITelephony."); + } + + Object result = method.getParameterCount() == 2 + ? method.invoke(phone, simInfo.subId, networkMode.getLegacyMode()) + : method.invoke(phone, simInfo.subId, networkMode.getLegacyMode(), PACKAGE); + + if (result instanceof Boolean && !((Boolean) result)) { + return CommandResult.failed("AIDL: setPreferredNetworkType", "ITelephony returned false."); } - } catch (Exception e) { - e.printStackTrace(); - return CommandResult.failed("AIDL Binder Call", "Exception: " + e.toString()); + + return CommandResult.completed("AIDL: setPreferredNetworkType", 0, "Legacy network mode applied successfully via Shizuku Binder.", ""); + + } catch (Throwable throwable) { + return CommandResult.failed("AIDL Binder Call", TelephonyMethodHelper.describe(throwable)); } } } From c5b3930347c3475aefdc4bc4929dab3ed5b4aebe Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:03:49 +0530 Subject: [PATCH 048/394] Fortify ShizukuBinderModeReader reflection - Replaced hardcoded method signature expectations with TelephonyMethodHelper.find(). - Added dynamic fallback support for OEM-modified getAllowedNetworkTypesForReason and getPreferredNetworkType getters. - Secured the reflection loops against NullPointerExceptions and Binder crash edge cases. --- .../telephony/ShizukuBinderModeReader.java | 144 +++++++++--------- 1 file changed, 71 insertions(+), 73 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeReader.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeReader.java index 9c7f885..cc15611 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeReader.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeReader.java @@ -18,6 +18,8 @@ import rikka.shizuku.SystemServiceHelper; final class ShizukuBinderModeReader { + + private static final String PACKAGE = "com.dhangofa.networktoggle"; private final SimResolver simResolver; ShizukuBinderModeReader(SimResolver simResolver) { @@ -29,8 +31,9 @@ NetworkMode readCurrentMode(ExecutionMode executionMode) { return NetworkMode.UNKNOWN; } - int targetSubId = simResolver.resolveTargetSubId(executionMode); - if (!simResolver.isValidSubId(targetSubId)) { + int subId = simResolver.resolveTargetSubId(executionMode); + + if (!simResolver.isValidSubId(subId)) { return NetworkMode.UNKNOWN; } @@ -39,92 +42,87 @@ NetworkMode readCurrentMode(ExecutionMode executionMode) { org.lsposed.hiddenapibypass.HiddenApiBypass.addHiddenApiExemptions("Lcom/android/internal/telephony/"); } - IBinder binder = new ShizukuBinderWrapper(SystemServiceHelper.getSystemService("phone")); - if (binder == null) { + IBinder raw = SystemServiceHelper.getSystemService("phone"); + if (raw == null || !raw.pingBinder()) { return NetworkMode.UNKNOWN; } - @SuppressLint("PrivateApi") - Class iTelephonyStubClass = Class.forName("com.android.internal.telephony.ITelephony$Stub"); - Method asInterfaceMethod = iTelephonyStubClass.getDeclaredMethod("asInterface", IBinder.class); - Object iTelephony = asInterfaceMethod.invoke(null, binder); - - if (iTelephony == null) { + IBinder binder = new ShizukuBinderWrapper(raw); + if (!binder.pingBinder()) { return NetworkMode.UNKNOWN; } - Class iTelephonyClass = Class.forName("com.android.internal.telephony.ITelephony"); + @SuppressLint("PrivateApi") + Class stub = Class.forName("com.android.internal.telephony.ITelephony$Stub"); + Object phone = stub.getDeclaredMethod("asInterface", IBinder.class).invoke(null, binder); + + Class api = Class.forName("com.android.internal.telephony.ITelephony"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - Method getAllowedMethod = null; - for (Method m : iTelephonyClass.getDeclaredMethods()) { - if (m.getName().equals("getAllowedNetworkTypesForReason")) { - getAllowedMethod = m; - break; - } + Method method = TelephonyMethodHelper.find( + api, + "getAllowedNetworkTypesForReason", + new Class[] {int.class, int.class}, + new Class[] {int.class, int.class, String.class}, + new Class[] {int.class} + ); + + if (method == null) { + return NetworkMode.UNKNOWN; } - if (getAllowedMethod != null) { - Class[] pTypes = getAllowedMethod.getParameterTypes(); - long bitmask = -1; - if (pTypes.length == 2) { - bitmask = (long) getAllowedMethod.invoke(iTelephony, targetSubId, 0 /* reason user */); - } else if (pTypes.length == 3 && pTypes[2] == String.class) { - bitmask = (long) getAllowedMethod.invoke(iTelephony, targetSubId, 0, "com.dhangofa.networktoggle"); - } else if (pTypes.length == 1) { - bitmask = (long) getAllowedMethod.invoke(iTelephony, targetSubId); - } - - if (bitmask != -1) { - // Find closest exact matching NetworkMode by bitmask - for (NetworkMode mode : NetworkMode.values()) { - if (mode.getBinaryMask() != null) { - long modeMask = Long.parseLong(mode.getBinaryMask(), 2); - if (modeMask == bitmask) { - return mode; - } - } - } - - // Approximate mapping if exact bitmask fails - if ((bitmask & (1L << 19 /* NR */)) != 0) { - if ((bitmask & (1L << 12 /* LTE */)) == 0) return NetworkMode.FIVE_G_ONLY; - return NetworkMode.PREFERRED_5G; - } - if ((bitmask & (1L << 12 /* LTE */)) != 0) { - if ((bitmask & (1L << 13 /* TD_SCDMA */)) == 0 && (bitmask & (1L << 9 /* WCDMA */)) == 0) return NetworkMode.FOUR_G_ONLY; - return NetworkMode.PREFERRED_4G; - } - return NetworkMode.PREFERRED_3G; - } + Object value = method.getParameterCount() == 2 + ? method.invoke(phone, subId, 0) + : method.getParameterCount() == 3 + ? method.invoke(phone, subId, 0, PACKAGE) + : method.invoke(phone, subId); + + if (!(value instanceof Number)) { + return NetworkMode.UNKNOWN; } - } else { - Method getPrefMethod = null; - for (Method m : iTelephonyClass.getDeclaredMethods()) { - if (m.getName().equals("getPreferredNetworkType")) { - getPrefMethod = m; - break; + + long bitmask = ((Number) value).longValue(); + + for (NetworkMode mode : NetworkMode.values()) { + if (mode.getBinaryMask() != null && Long.parseLong(mode.getBinaryMask(), 2) == bitmask) { + return mode; } } - - if (getPrefMethod != null) { - Class[] pTypes = getPrefMethod.getParameterTypes(); - int legacyMode = -1; - if (pTypes.length == 1) { - legacyMode = (int) getPrefMethod.invoke(iTelephony, targetSubId); - } else if (pTypes.length == 2 && pTypes[1] == String.class) { - legacyMode = (int) getPrefMethod.invoke(iTelephony, targetSubId, "com.dhangofa.networktoggle"); - } - - if (legacyMode != -1) { - return NetworkMode.fromLegacyMode(legacyMode); - } + + if ((bitmask & (1L << 19)) != 0) { + return (bitmask & (1L << 12)) == 0 ? NetworkMode.FIVE_G_ONLY : NetworkMode.PREFERRED_5G; + } + + if ((bitmask & (1L << 12)) != 0) { + return (bitmask & (1L << 13)) == 0 && (bitmask & (1L << 9)) == 0 + ? NetworkMode.FOUR_G_ONLY + : NetworkMode.PREFERRED_4G; } + + return NetworkMode.PREFERRED_3G; } - } catch (Exception e) { - e.printStackTrace(); - } - return NetworkMode.UNKNOWN; + Method method = TelephonyMethodHelper.find( + api, + "getPreferredNetworkType", + new Class[] {int.class}, + new Class[] {int.class, String.class} + ); + + if (method == null) { + return NetworkMode.UNKNOWN; + } + + Object value = method.getParameterCount() == 1 + ? method.invoke(phone, subId) + : method.invoke(phone, subId, PACKAGE); + + return value instanceof Number + ? NetworkMode.fromLegacyMode(((Number) value).intValue()) + : NetworkMode.UNKNOWN; + + } catch (Throwable ignored) { + return NetworkMode.UNKNOWN; + } } } From a68aae5c57faff1d4ae2c54ce2f802e61eda7c9e Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:21:22 +0530 Subject: [PATCH 049/394] Refactor GitHub Actions workflow for NetToggle - Dynamic Workflow Naming: Added a run-name expression to display "Build NetToggle for New Release" when manually triggered on main, and "Build NetToggle for Testing" on other branches. - Preserved PR Behavior: Ensured Pull Requests still display their native PR title in the Actions tab. - F-Droid Safe Versioning: Injected a sed script step to automatically append the commit hash to versionName and set versionCode to the GitHub run number for test builds. This occurs at the runner level, ensuring app/build.gradle.kts remains strictly hardcoded in the repository for F-Droid compatibility. - Preserved Artifact/Signing Isolation: Maintained the existing if: github.event_name == 'workflow_dispatch' constraints, ensuring PRs continue to compile the app without attempting to sign or upload artifacts. --- .github/workflows/build.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da331db..4d76b03 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,6 @@ -name: Build and Automate NetToggle Release +name: Build NetToggle + +run-name: ${{ github.event_name == 'workflow_dispatch' && (github.ref_name == 'main' && 'Build NetToggle for New Release' || 'Build NetToggle for Testing') || github.event.pull_request.title }} on: workflow_dispatch: @@ -38,6 +40,17 @@ jobs: - name: Make Gradle Wrapper Executable run: chmod +x ./gradlew + - name: Inject Automated Versioning (Test Builds) + if: github.event_name == 'workflow_dispatch' && github.ref_name != 'main' + run: | + SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) + RUN_NUM=${{ github.run_number }} + sed -i -E "s/versionCode *= *[0-9]+/versionCode = $RUN_NUM/" app/build.gradle.kts + sed -i -E "s/versionName *= *\"([^\"]+)\"/versionName = \"\1-test-$SHORT_SHA\"/" app/build.gradle.kts + + echo "Injected Test Versioning:" + grep -E "versionCode|versionName" app/build.gradle.kts + - name: Compile Release APK run: ./gradlew assembleRelease From edd612b439f263574c0626331224c912672ff273 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Fri, 28 Aug 2026 00:24:59 +0530 Subject: [PATCH 050/394] Implement pure Binder nudge to force SystemUI refresh - Injected a new triggerUiRefreshNudge() method that executes immediately after a successful network mode switch. - Utilized Shizuku to grab a direct IBinder reference to the system's telephony.registry service. - Safely invoked the hidden notifyCarrierNetworkChange (and notifyCarrierNetworkChangeWithSubId fallback) methods via reflection. - Forces heavily skinned ROMs (OriginOS, One UI, ColorOS) to flush their delayed ServiceState caches, instantly updating the status bar for VoLTE, VoNR, and Operator Name without dropping data or spiking CPU usage. --- .../ShizukuBinderModeController.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeController.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeController.java index 076c327..d6fa4c5 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeController.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShizukuBinderModeController.java @@ -96,6 +96,7 @@ CommandResult apply(NetworkMode networkMode, ExecutionMode executionMode) { return CommandResult.failed("AIDL: setAllowedNetworkTypesForReason", "ITelephony returned false."); } + triggerUiRefreshNudge(simInfo.subId); return CommandResult.completed("AIDL: setAllowedNetworkTypesForReason", 0, "Network mode applied successfully via Shizuku Binder.", ""); } @@ -122,10 +123,61 @@ CommandResult apply(NetworkMode networkMode, ExecutionMode executionMode) { return CommandResult.failed("AIDL: setPreferredNetworkType", "ITelephony returned false."); } + triggerUiRefreshNudge(simInfo.subId); return CommandResult.completed("AIDL: setPreferredNetworkType", 0, "Legacy network mode applied successfully via Shizuku Binder.", ""); } catch (Throwable throwable) { return CommandResult.failed("AIDL Binder Call", TelephonyMethodHelper.describe(throwable)); } } + + /** + * Directly pings the telephony.registry Binder to force SystemUI to redraw the network icons. + * This bypasses OEM caching delays (e.g. Samsung/ColorOS 5G icon smoothing) with zero CPU overhead. + */ + private void triggerUiRefreshNudge(int subId) { + try { + IBinder rawRegistry = SystemServiceHelper.getSystemService("telephony.registry"); + if (rawRegistry == null || !rawRegistry.pingBinder()) { + return; + } + + IBinder registryBinder = new ShizukuBinderWrapper(rawRegistry); + + @SuppressLint("PrivateApi") + Class stub = Class.forName("com.android.internal.telephony.ITelephonyRegistry$Stub"); + Object registry = stub.getDeclaredMethod("asInterface", IBinder.class).invoke(null, registryBinder); + if (registry == null) { + return; + } + + Method notifyCarrier = TelephonyMethodHelper.find( + registry.getClass(), + "notifyCarrierNetworkChange", + new Class[] {boolean.class} + ); + + if (notifyCarrier == null) { + notifyCarrier = TelephonyMethodHelper.find( + registry.getClass(), + "notifyCarrierNetworkChangeWithSubId", + new Class[] {int.class, boolean.class} + ); + } + + if (notifyCarrier != null) { + if (notifyCarrier.getParameterCount() == 2) { + notifyCarrier.invoke(registry, subId, true); + Thread.sleep(50); + notifyCarrier.invoke(registry, subId, false); + } else { + notifyCarrier.invoke(registry, true); + Thread.sleep(50); + notifyCarrier.invoke(registry, false); + } + } + } catch (Throwable ignored) { + // Nudge is strictly a best-effort UX improvement. Safely ignore failures. + } + } } From 9aa0e362f3901ade3785289084ed82b6d64bc159 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:52:34 +0530 Subject: [PATCH 051/394] Add AutomationReceiver and ShortcutActionActivity - Registered AutomationReceiver for Tasker/MacroDroid like apps - Registered ShortcutActionActivity for app shortcuts - Updated NetworkTileService icon --- app/src/main/AndroidManifest.xml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 70dc52b..5adcf4b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,6 +37,25 @@ android:exported="true" android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" /> + + + + + + + + + + From 1febf269c915813fc8c870c4bd1de4a8947677f5 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:54:54 +0530 Subject: [PATCH 052/394] Add AutomationReceiver to handle network mode changes - Added broadcast receiver for automation integrations --- .../networktoggle/AutomationReceiver.java | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/AutomationReceiver.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/AutomationReceiver.java b/app/src/main/java/com/dhangofa/networktoggle/AutomationReceiver.java new file mode 100644 index 0000000..efd5852 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/AutomationReceiver.java @@ -0,0 +1,180 @@ +package com.dhangofa.networktoggle; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +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 com.dhangofa.networktoggle.telephony.NetworkModeController; +import com.dhangofa.networktoggle.telephony.SimResolver; +import com.dhangofa.networktoggle.util.AppExecutors; + +public class AutomationReceiver extends BroadcastReceiver { + + private static final String TAG = "AutomationReceiver"; + private static final String ACTION_SET_MODE = "com.dhangofa.networktoggle.SET_MODE"; + private static final String EXTRA_MODE = "mode"; + private static final String EXTRA_SIM = "sim"; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null || !ACTION_SET_MODE.equals(intent.getAction())) { + return; + } + + String modeString = intent.getStringExtra(EXTRA_MODE); + if (modeString == null || modeString.isEmpty()) { + Log.e(TAG, "No mode provided in intent extra '" + EXTRA_MODE + "'"); + return; + } + + NetworkMode targetMode = parseNetworkMode(modeString); + if (targetMode == NetworkMode.UNKNOWN) { + Log.e(TAG, "Unknown mode requested: " + modeString); + return; + } + + final PendingResult pendingResult = goAsync(); + + AppExecutors.executeTelephony(() -> { + try { + AppPreferences prefs = new AppPreferences(context); + ExecutionMode execMode = prefs.getExecutionMode(); + SimResolver simResolver = new SimResolver(context, prefs); + + int simOverride = intent.getIntExtra(EXTRA_SIM, -1); + + TargetSim targetSim = prefs.getTargetSim(); + if (simOverride == 1) targetSim = TargetSim.SIM_1; + else if (simOverride == 2) targetSim = TargetSim.SIM_2; + else if (simOverride == 3) targetSim = TargetSim.BOTH; + + // Explicit Safeguard: Verify the physical SIM slot is valid and active + // before doing anything, so we never cross-apply accidentally. + if (targetSim == TargetSim.SIM_1 || targetSim == TargetSim.SIM_2) { + simResolver.setOverrideTargetSim(targetSim); + SimResolver.SimInfo info = simResolver.resolveTargetSimInfo(execMode); + if (info == null || info.slotIndex != targetSim.getManualSlotIndex()) { + Log.e(TAG, "Rejected: Requested SIM " + (targetSim.getManualSlotIndex() + 1) + " is not available or removed."); + return; // Abort execution + } + } + + AppPreferences.NetworkCapabilities caps; + if (targetSim == TargetSim.BOTH) { + AppPreferences.NetworkCapabilities sim1Caps = prefs.getSlotCapabilities(0); + AppPreferences.NetworkCapabilities sim2Caps = prefs.getSlotCapabilities(1); + if (sim1Caps == null) sim1Caps = prefs.getDeviceCapabilities(); + if (sim2Caps == null) sim2Caps = prefs.getDeviceCapabilities(); + + if (sim1Caps == null || sim2Caps == null) { + caps = AppPreferences.NetworkCapabilities.assumeAll(); + } else { + caps = new AppPreferences.NetworkCapabilities( + sim1Caps.supports2g && sim2Caps.supports2g, + sim1Caps.supports3g && sim2Caps.supports3g, + sim1Caps.supports4g && sim2Caps.supports4g, + sim1Caps.supports5g && sim2Caps.supports5g + ); + } + } else { + int slotIndex = targetSim.getManualSlotIndex(); + caps = prefs.getSlotCapabilities(slotIndex); + if (caps == null) caps = prefs.getDeviceCapabilities(); + if (caps == null) caps = AppPreferences.NetworkCapabilities.assumeAll(); + } + + boolean supported = false; + switch (targetMode) { + case FIVE_G_ONLY: + case PREFERRED_5G: + supported = caps.supports5g; + break; + case FOUR_G_ONLY: + case PREFERRED_4G: + supported = caps.supports4g; + break; + case PREFERRED_3G: + supported = caps.supports3g; + break; + case TWO_G_ONLY: + supported = caps.supports2g; + break; + default: + supported = true; + } + + if (!supported) { + Log.e(TAG, "Rejected requested mode " + targetMode.getDisplayName() + " as it is not supported by the selected SIM(s)."); + return; // Abort execution + } + + NetworkModeController controller = new NetworkModeController(simResolver); + + if (targetSim == TargetSim.BOTH) { + // Modern APIs execute per-slot, we must loop to execute on BOTH + boolean success = true; + + simResolver.setOverrideTargetSim(TargetSim.SIM_1); + if (simResolver.resolveTargetSimInfo(execMode) != null) { + CommandResult r1 = controller.apply(targetMode, execMode); + if (!r1.isSuccess()) success = false; + } + + simResolver.setOverrideTargetSim(TargetSim.SIM_2); + if (simResolver.resolveTargetSimInfo(execMode) != null) { + CommandResult r2 = controller.apply(targetMode, execMode); + if (!r2.isSuccess()) success = false; + } + + if (success) { + Log.i(TAG, "Successfully changed network mode to " + targetMode.getDisplayName() + " on Both SIMs."); + } else { + Log.e(TAG, "Failed to change network mode on one or both SIMs."); + } + } else { + simResolver.setOverrideTargetSim(targetSim); + CommandResult result = controller.apply(targetMode, execMode); + if (result.isSuccess()) { + Log.i(TAG, "Successfully changed network mode to " + targetMode.getDisplayName() + " via automation intent."); + } else { + Log.e(TAG, "Failed to change network mode via automation: " + result.getStderr()); + } + } + } finally { + pendingResult.finish(); + } + }); + } + + private NetworkMode parseNetworkMode(String modeString) { + modeString = modeString.toUpperCase().trim(); + switch (modeString) { + case "5G_ONLY": + case "5G": + return NetworkMode.FIVE_G_ONLY; + case "4G_ONLY": + case "4G": + case "LTE": + return NetworkMode.FOUR_G_ONLY; + case "PREF_5G": + case "PREFERRED_5G": + return NetworkMode.PREFERRED_5G; + case "PREF_4G": + case "PREFERRED_4G": + return NetworkMode.PREFERRED_4G; + case "PREF_3G": + case "PREFERRED_3G": + return NetworkMode.PREFERRED_3G; + case "2G_ONLY": + case "2G": + return NetworkMode.TWO_G_ONLY; + default: + return NetworkMode.UNKNOWN; + } + } +} From 6403bbaf6265e49fc7a1702a8c9fa90be5454b28 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:57:09 +0530 Subject: [PATCH 053/394] Add methods for saving and retrieving routine shortcuts - Added saveRoutineShortcut method - Added retrieval methods for routine shortcut mode and sim --- .../networktoggle/config/AppPreferences.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java b/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java index 988b24d..7cf6153 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java +++ b/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java @@ -274,4 +274,19 @@ public void invalidateSlotCache(int slotIndex) { .remove(KEY_SLOT_CAPS_PREFIX + slotIndex + "_5g") .apply(); } + + public void saveRoutineShortcut(int slot, String mode, int sim) { + preferences.edit() + .putString("routine_shortcut_mode_" + slot, mode) + .putInt("routine_shortcut_sim_" + slot, sim) + .apply(); + } + + public String getRoutineShortcutMode(int slot) { + return preferences.getString("routine_shortcut_mode_" + slot, "NONE"); + } + + public int getRoutineShortcutSim(int slot) { + return preferences.getInt("routine_shortcut_sim_" + slot, 1); // 1 = SIM 1 + } } From 3961ed58841039c271a4824f14047a2eb0a0dd50 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:02:05 +0530 Subject: [PATCH 054/394] Refactored main activity to support theming and new features - Refactor theme management and remove unused SIM selection UI. - Added multi-state theme toggle (Light/Dark/AMOLED) - Added Routine Shortcuts Floating Action Button (FAB) - Updated UI controllers for localized strings - Added animation logic for Floating Action Button (FAB) --- .../dhangofa/networktoggle/MainActivity.java | 314 ++++++++++-------- 1 file changed, 170 insertions(+), 144 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java index 18088be..2327eff 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java +++ b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java @@ -15,6 +15,7 @@ package com.dhangofa.networktoggle; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -22,8 +23,11 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.view.View; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.content.res.ColorStateList; import android.widget.ImageView; +import android.view.View; import android.widget.LinearLayout; import android.widget.RadioButton; import android.widget.RadioGroup; @@ -46,7 +50,6 @@ import android.view.Gravity; import android.view.ViewGroup; import android.view.Window; -import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.content.ClipData; import android.content.ClipboardManager; @@ -57,7 +60,56 @@ import rikka.shizuku.Shizuku; -public class MainActivity extends Activity implements android.content.SharedPreferences.OnSharedPreferenceChangeListener { +public class MainActivity extends Activity implements android.content.SharedPreferences.OnSharedPreferenceChangeListener { + private int currentThemeMode = -1; + private ImageView btnThemeToggle; + + @Override + protected void attachBaseContext(Context newBase) { + SharedPreferences prefs = newBase.getSharedPreferences("AppPrefs", MODE_PRIVATE); + int mode = prefs.getInt("app_theme", 0); + + Configuration config = new Configuration(newBase.getResources().getConfiguration()); + if (mode == 1) { // Light + config.uiMode = (config.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | Configuration.UI_MODE_NIGHT_NO; + } else if (mode == 2 || mode == 3) { // Dark or AMOLED + config.uiMode = (config.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | Configuration.UI_MODE_NIGHT_YES; + } + super.attachBaseContext(newBase.createConfigurationContext(config)); + } + + private void cycleThemeMode() { + currentThemeMode = (currentThemeMode + 1) % 4; + getSharedPreferences("AppPrefs", MODE_PRIVATE).edit().putInt("app_theme", currentThemeMode).apply(); + recreate(); + } + + private void updateThemeIcon() { + if (btnThemeToggle == null) return; + if (currentThemeMode == 0) { + btnThemeToggle.setImageResource(R.drawable.ic_theme_auto); + } else if (currentThemeMode == 1) { + btnThemeToggle.setImageResource(R.drawable.ic_theme_light); + } else if (currentThemeMode == 2) { + btnThemeToggle.setImageResource(R.drawable.ic_theme_dark); + } else { + btnThemeToggle.setImageResource(R.drawable.ic_theme_amoled); + } + } + + private void applyAmoledIfNeeded() { + if (currentThemeMode == 3) { + int black = Color.BLACK; + View root = findViewById(R.id.mainRoot); + if (root != null) root.setBackgroundColor(black); + getWindow().setNavigationBarColor(black); + + // AMOLED mode only darkens the root window background to pure black to save battery. + // Cards and surfaces will retain their default dark mode elevation and borders, + // otherwise they become completely invisible against the black background. + } + } + private RadioGroup radioGroup; private RadioButton radioRoot; @@ -66,21 +118,12 @@ public class MainActivity extends Activity implements android.content.SharedPref private TextView statusText; private ImageView githubLink; private ImageView telegramLink; - - private RadioGroup targetSimRadioGroup; - private RadioButton radioSimAuto; - private RadioButton radioSim1; - private RadioButton radioSim2; - private TextView autoSimWarningText; - private ImageView faqLink; // Morphing View Carousel private com.dhangofa.networktoggle.ui.CarouselManager carouselManager; private View separatorRootShizuku; - private View separatorAutoSim1; - private View separatorSim1Sim2; private com.dhangofa.networktoggle.ui.PhoneStatePermissionManager permissionManager; private View errorBannerContainer; @@ -94,25 +137,41 @@ public class MainActivity extends Activity implements android.content.SharedPref private volatile boolean activityDestroyed; private com.dhangofa.networktoggle.ui.ExecutionStateController executionStateController; private TargetSimUiController targetSimUiController; + private SimResolver simResolver; + private NetworkModeController modeController; + private com.dhangofa.networktoggle.telephony.NetworkModeReader modeReader; + private TargetSim lastTargetSim = TargetSim.AUTO; @Override protected void onCreate(Bundle savedInstanceState) { + android.content.SharedPreferences prefs = getSharedPreferences("AppPrefs", MODE_PRIVATE); + currentThemeMode = prefs.getInt("app_theme", 0); + super.onCreate(savedInstanceState); activityDestroyed = false; // Triggering fresh deploy to streaming emulator to clear cache/state configureStatusBar(); setContentView(R.layout.activity_main); + + btnThemeToggle = (ImageView) findViewById(R.id.btnThemeToggle); + updateThemeIcon(); + if (btnThemeToggle != null) { + btnThemeToggle.setOnClickListener(v -> cycleThemeMode()); + } + applyAmoledIfNeeded(); appPreferences = new AppPreferences(this); + lastTargetSim = appPreferences.getTargetSim(); appPreferences.registerListener(this); bindViews(); - appVersionText.setText("v" + getAppVersionName()); + appVersionText.setText(getString(R.string.app_version_format, getAppVersionName())); bindLinks(); - SimResolver simResolver = new SimResolver(this, appPreferences); + simResolver = new SimResolver(this, appPreferences); capabilityResolver = new NetworkCapabilityResolver(appPreferences, simResolver); TileCycleManager tileCycleManager = new TileCycleManager(appPreferences); tileCycleUiController = new TileCycleUiController(this, tileCycleManager); - NetworkModeController modeController = new NetworkModeController(simResolver); + modeController = new NetworkModeController(simResolver); + modeReader = new com.dhangofa.networktoggle.telephony.NetworkModeReader(this, appPreferences, simResolver); permissionManager = new com.dhangofa.networktoggle.ui.PhoneStatePermissionManager(this, REQ_CODE_PHONE_STATE, this::updateCapabilities); @@ -125,7 +184,7 @@ protected void onCreate(Bundle savedInstanceState) { tileCycleUiController.initialize(); executionStateController.registerListeners(); targetSimUiController = new TargetSimUiController( - this, appPreferences, this::updateCapabilities); + this, appPreferences, this::onTargetSimSelectionChanged); targetSimUiController.initialize(); bindSelectionListeners(); updateSeparatorVisibility(); @@ -142,36 +201,63 @@ private void configureStatusBar() { boolean isNight = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; - getWindow().setStatusBarColor(getColor(R.color.card_surface)); - getWindow().getDecorView().setSystemUiVisibility( - isNight ? 0 : View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + + android.view.Window window = getWindow(); + window.setStatusBarColor(getColor(R.color.card_surface)); + + if (currentThemeMode == 3) { + window.setNavigationBarColor(Color.BLACK); + } else { + window.setNavigationBarColor(getColor(R.color.surface_background)); + } + + int flags = isNight ? 0 : View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + if (!isNight && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; + } + window.getDecorView().setSystemUiVisibility(flags); } + private View btnRoutineShortcuts; + private void bindViews() { radioGroup = findViewById(R.id.modeRadioGroup); radioRoot = findViewById(R.id.radioRoot); radioShizuku = findViewById(R.id.radioShizuku); statusText = findViewById(R.id.shizukuStatusText); appVersionText = findViewById(R.id.appVersionText); - targetSimRadioGroup = findViewById(R.id.targetSimRadioGroup); - radioSimAuto = findViewById(R.id.radioSimAuto); - radioSim1 = findViewById(R.id.radioSim1); - radioSim2 = findViewById(R.id.radioSim2); - autoSimWarningText = findViewById(R.id.autoSimWarningText); + faqLink = findViewById(R.id.faqLink); githubLink = findViewById(R.id.githubLink); telegramLink = findViewById(R.id.telegramLink); - - faqLink = findViewById(R.id.faqLink); separatorRootShizuku = findViewById(R.id.separatorRootShizuku); - separatorAutoSim1 = findViewById(R.id.separatorAutoSim1); - separatorSim1Sim2 = findViewById(R.id.separatorSim1Sim2); errorBannerContainer = findViewById(R.id.cardErrorBanner); if (errorBannerContainer != null) { errorBannerContainer.setOnClickListener(v -> showDiagnosticDialog()); } - + btnRoutineShortcuts = findViewById(R.id.btnRoutineShortcuts); + if (btnRoutineShortcuts != null) { + btnRoutineShortcuts.setOnClickListener(v -> com.dhangofa.networktoggle.ui.ShortcutDialogHelper.showDialog(this, appPreferences)); + + android.widget.ScrollView mainScrollView = findViewById(R.id.mainScrollView); + if (mainScrollView != null) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + mainScrollView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { + if (scrollY > oldScrollY && btnRoutineShortcuts.getScaleX() == 1f) { + // Scrolling down (Swiping up) -> Hide FAB + btnRoutineShortcuts.animate().scaleX(0f).scaleY(0f).setDuration(200).withEndAction(() -> { + btnRoutineShortcuts.setVisibility(View.INVISIBLE); + }).start(); + } else if (scrollY < oldScrollY && btnRoutineShortcuts.getScaleX() == 0f) { + // Scrolling up (Swiping down) -> Show FAB + btnRoutineShortcuts.setVisibility(View.VISIBLE); + btnRoutineShortcuts.animate().scaleX(1f).scaleY(1f).setDuration(200).withEndAction(null).start(); + } + }); + } + } + } } private void bindLinks() { @@ -204,22 +290,6 @@ private void updateSeparatorVisibility() { } else { separatorRootShizuku.setVisibility(View.INVISIBLE); } - - // Target SIM Separators - int simId = targetSimRadioGroup.getCheckedRadioButtonId(); - if (simId == -1) { - separatorAutoSim1.setVisibility(View.VISIBLE); - separatorSim1Sim2.setVisibility(View.VISIBLE); - } else if (simId == R.id.radioSimAuto) { - separatorAutoSim1.setVisibility(View.INVISIBLE); - separatorSim1Sim2.setVisibility(View.VISIBLE); - } else if (simId == R.id.radioSim1) { - separatorAutoSim1.setVisibility(View.INVISIBLE); - separatorSim1Sim2.setVisibility(View.INVISIBLE); - } else if (simId == R.id.radioSim2) { - separatorAutoSim1.setVisibility(View.VISIBLE); - separatorSim1Sim2.setVisibility(View.INVISIBLE); - } } private void loadSavedExecutionMode() { @@ -232,12 +302,10 @@ private void loadSavedExecutionMode() { executionStateController.checkShizukuPermission(false); } else { radioGroup.clearCheck(); - setStatus("Select Root or Shizuku mode.", 0xFFFFB300); + setStatus(getString(R.string.status_select_mode), 3); } } - private boolean updatingSimUi = false; - private void bindSelectionListeners() { radioGroup.setOnCheckedChangeListener((group, checkedId) -> { updateSeparatorVisibility(); @@ -249,53 +317,6 @@ private void bindSelectionListeners() { executionStateController.checkShizukuPermission(true); } }); - - android.view.View.OnTouchListener lockTouch = (v, event) -> { - if (!isUIAuthorized && event.getAction() == android.view.MotionEvent.ACTION_DOWN) { - android.widget.Toast.makeText(MainActivity.this, "Please authorize Root or Shizuku to configure toggles.", android.widget.Toast.LENGTH_SHORT).show(); - return true; - } - return false; - }; - radioSimAuto.setOnTouchListener(lockTouch); - radioSim1.setOnTouchListener(lockTouch); - radioSim2.setOnTouchListener(lockTouch); - - targetSimRadioGroup.setOnCheckedChangeListener((group, checkedId) -> { - if (updatingSimUi) return; - TargetSim target = TargetSim.AUTO; - if (checkedId == R.id.radioSim1) target = TargetSim.SIM_1; - else if (checkedId == R.id.radioSim2) target = TargetSim.SIM_2; - - // Validate slot immediately - if (target != TargetSim.AUTO && checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) { - android.telephony.SubscriptionManager sm = getSystemService(android.telephony.SubscriptionManager.class); - if (sm != null) { - boolean found = false; - java.util.List infos = sm.getActiveSubscriptionInfoList(); - if (infos != null) { - for (android.telephony.SubscriptionInfo info : infos) { - if (info.getSimSlotIndex() == target.getManualSlotIndex()) { - found = true; - break; - } - } - } - if (!found) { - Toast.makeText(MainActivity.this, "No SIM card found in slot " + (target.getManualSlotIndex() + 1), Toast.LENGTH_SHORT).show(); - updatingSimUi = true; - radioSimAuto.setChecked(true); - updatingSimUi = false; - target = TargetSim.AUTO; - } - } - } - - updateSeparatorVisibility(); - appPreferences.onTargetSimChanged(target); - updateAutoSimWarning(); - updateCapabilities(); - }); } @Override @@ -334,6 +355,41 @@ private void showDiagnosticDialog() { diagnosticDialog.show(); } + private void onTargetSimSelectionChanged() { + TargetSim newTarget = appPreferences.getTargetSim(); + if (lastTargetSim != TargetSim.BOTH && newTarget == TargetSim.BOTH) { + AppExecutors.executeTelephony(() -> { + simResolver.setOverrideTargetSim(TargetSim.SIM_1); + NetworkMode mode1 = modeReader.readCurrentMode(); + + simResolver.setOverrideTargetSim(TargetSim.SIM_2); + NetworkMode mode2 = modeReader.readCurrentMode(); + simResolver.setOverrideTargetSim(null); + + if (mode1 != NetworkMode.UNKNOWN && mode2 != NetworkMode.UNKNOWN && mode1 != mode2) { + com.dhangofa.networktoggle.cycle.TileCycleManager tileCycleManager = new com.dhangofa.networktoggle.cycle.TileCycleManager(appPreferences); + AppPreferences.NetworkCapabilities combinedCaps = capabilityResolver.getCapabilities(appPreferences.getExecutionMode()); + tileCycleManager.forceRemoveUnsupportedAndAutoFill(combinedCaps); + java.util.List cycle = tileCycleManager.getCycle(); + NetworkMode modeToApply = cycle.contains(mode1) ? mode1 : (cycle.contains(mode2) ? mode2 : cycle.get(0)); + + simResolver.setOverrideTargetSim(TargetSim.SIM_1); + modeController.apply(modeToApply, appPreferences.getExecutionMode()); + simResolver.setOverrideTargetSim(TargetSim.SIM_2); + modeController.apply(modeToApply, appPreferences.getExecutionMode()); + simResolver.setOverrideTargetSim(null); + + appPreferences.setCachedNetworkMode(NetworkMode.UNKNOWN); + } + + updateCapabilities(); + }); + } else { + updateCapabilities(); + } + lastTargetSim = newTarget; + } + private void updateCapabilities() { AppExecutors.executeTelephony(() -> { AppPreferences.NetworkCapabilities caps = capabilityResolver.getCapabilities(appPreferences.getExecutionMode()); @@ -341,6 +397,7 @@ private void updateCapabilities() { if (!activityDestroyed && tileCycleUiController != null) { tileCycleUiController.applyCapabilities(caps); } + android.service.quicksettings.TileService.requestListeningState(MainActivity.this, new android.content.ComponentName(MainActivity.this, NetworkTileService.class)); }); }); } @@ -377,47 +434,35 @@ public void onSharedPreferenceChanged(android.content.SharedPreferences sharedPr } - private void setStatus(String text, int color) { + private void setStatus(String text, int colorCode) { statusText.setText(text); - statusText.setTextColor(color); - if (color == 0xFF1B873F) { - statusText.setBackgroundResource( - R.drawable.shape_status_badge_success - ); - } else if (color == 0xFFFF5555) { - statusText.setBackgroundResource( - R.drawable.shape_status_badge_error - ); - } else { - statusText.setBackgroundResource( - R.drawable.shape_pill_badge_bg - ); + if (colorCode == 1) { // Success + statusText.setTextColor(getColor(R.color.status_success_text)); + statusText.setBackgroundResource(R.drawable.shape_status_badge_success); + } else if (colorCode == 2) { // Error + statusText.setTextColor(getColor(R.color.status_error_text)); + statusText.setBackgroundResource(R.drawable.shape_status_badge_error); + } else { // Warning/Neutral + statusText.setTextColor(getColor(R.color.status_warning_text)); + statusText.setBackgroundResource(R.drawable.shape_status_badge_warning); } - updateAuthorizationUI(color == 0xFF1B873F); + updateAuthorizationUI(colorCode == 1); } - - - - - - - - private boolean isUIAuthorized = true; + private Boolean isUIAuthorized = null; private void updateAuthorizationUI(boolean authorized) { - if (isUIAuthorized == authorized) return; + if (isUIAuthorized != null && isUIAuthorized == authorized) return; isUIAuthorized = authorized; if (carouselManager != null) carouselManager.updateCarouselContext(authorized); float alpha = authorized ? 1.0f : 0.4f; - View targetSimCard = findViewById(R.id.targetSimRadioGroup); - if (targetSimCard != null) targetSimCard.setAlpha(alpha); - View targetSimHeader = findViewById(R.id.targetSimHeaderContainer); - if (targetSimHeader != null) targetSimHeader.setAlpha(alpha); - View autoSimWarningText = findViewById(R.id.autoSimWarningText); - if (autoSimWarningText != null) autoSimWarningText.setAlpha(alpha); + + if (btnRoutineShortcuts != null) { + btnRoutineShortcuts.setAlpha(alpha); + btnRoutineShortcuts.setEnabled(authorized); + } if (tileCycleUiController != null) { tileCycleUiController.setAuthorized(authorized); @@ -449,24 +494,5 @@ private void openUrl(String url) { } catch (Exception ignored) { } } - - private void loadSavedTargetSimMode() { - TargetSim target = appPreferences.getTargetSim(); - if (target == TargetSim.SIM_1) radioSim1.setChecked(true); - else if (target == TargetSim.SIM_2) radioSim2.setChecked(true); - else radioSimAuto.setChecked(true); - } - - private void updateAutoSimWarning() { - if (autoSimWarningText == null || appPreferences == null) return; - boolean show = appPreferences.hasAutoSimError() - && appPreferences.getTargetSim() == TargetSim.AUTO; - autoSimWarningText.setVisibility(show ? View.VISIBLE : View.GONE); - if (show) { - autoSimWarningText.setText( - "Auto SIM detection failed. Please choose SIM 1 or SIM 2 manually."); - autoSimWarningText.setTextColor(0xFFFF5555); - } - } } From c309b0262547e87da991b4e5699533953815b0ec Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:04:42 +0530 Subject: [PATCH 055/394] Add BOTH option to TargetSim enum - Added BOTH enum value to support dual-SIM simultaneous toggling --- .../main/java/com/dhangofa/networktoggle/model/TargetSim.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/model/TargetSim.java b/app/src/main/java/com/dhangofa/networktoggle/model/TargetSim.java index 6479af1..c99360a 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/model/TargetSim.java +++ b/app/src/main/java/com/dhangofa/networktoggle/model/TargetSim.java @@ -7,7 +7,8 @@ public enum TargetSim { AUTO(0, -1), SIM_1(1, 0), - SIM_2(2, 1); + SIM_2(2, 1), + BOTH(3, -1); private final int value; private final int manualSlotIndex; From 20f3862a606abef6195a5c5bc42d4fa970449fde Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:06:28 +0530 Subject: [PATCH 056/394] Updated tile service logic and localization - Handled TargetSim.BOTH by applying modes to both SIMs sequentially - Replaced hardcoded strings with context.getString() - Updated tile icon logic to support dynamic badges --- .../networktoggle/NetworkTileService.java | 94 +++++++++++++------ 1 file changed, 66 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java index 5f29ca8..9e921f9 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java +++ b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java @@ -146,22 +146,44 @@ public void onClick() { updateTileSwitchingUI(); AppExecutors.executeTelephony(() -> { - int slotIndex = simResolver.resolveTargetSlotIndex(executionMode); - if (!simResolver.isValidSlotIndex(slotIndex)) { - mainHandler.post(() -> { - Toast.makeText(getApplicationContext(), "No SIM card found in target slot.", Toast.LENGTH_SHORT).show(); - appPreferences.onTargetSimChanged(com.dhangofa.networktoggle.model.TargetSim.AUTO); - updateTileUI(appPreferences.getCachedNetworkMode()); - IS_SWITCHING.set(false); - }); - return; + CommandResult result; + + if (appPreferences.getTargetSim() == com.dhangofa.networktoggle.model.TargetSim.BOTH) { + simResolver.setOverrideTargetSim(com.dhangofa.networktoggle.model.TargetSim.SIM_1); + CommandResult result1 = networkModeController.apply(nextMode, executionMode); + + simResolver.setOverrideTargetSim(com.dhangofa.networktoggle.model.TargetSim.SIM_2); + CommandResult result2 = networkModeController.apply(nextMode, executionMode); + + simResolver.setOverrideTargetSim(null); + + if (result1.isSuccess() && result2.isSuccess()) { + result = CommandResult.completed("", 0, "Applied to both SIMs", ""); + } else if (result1.isSuccess()) { + result = CommandResult.failed("", "Failed to apply to SIM 2"); + } else if (result2.isSuccess()) { + result = CommandResult.failed("", "Failed to apply to SIM 1"); + } else { + result = result1; + } + } else { + int slotIndex = simResolver.resolveTargetSlotIndex(executionMode); + if (!simResolver.isValidSlotIndex(slotIndex)) { + mainHandler.post(() -> { + Toast.makeText(getApplicationContext(), getString(R.string.toast_no_sim_target_slot), Toast.LENGTH_SHORT).show(); + appPreferences.onTargetSimChanged(com.dhangofa.networktoggle.model.TargetSim.AUTO); + updateTileUI(appPreferences.getCachedNetworkMode()); + IS_SWITCHING.set(false); + }); + return; + } + + result = networkModeController.apply( + nextMode, + executionMode + ); } - CommandResult result = networkModeController.apply( - nextMode, - executionMode - ); - if (result.isSuccess()) { appPreferences.setCachedNetworkMode(nextMode); appPreferences.setLastNetworkCheckTimestamp(System.currentTimeMillis()); @@ -219,8 +241,8 @@ private void updateTileSwitchingUI() { if (tile == null) return; tile.setState(Tile.STATE_INACTIVE); - tile.setLabel("Switching..."); - tile.setIcon(TileIconManager.getCachedIcon("?")); + tile.setLabel(getString(R.string.tile_switching)); + tile.setIcon(TileIconManager.getCachedIcon("?", "", false)); tile.updateTile(); } @@ -234,20 +256,20 @@ private void updateTileUI(NetworkMode mode) { int errorState = appPreferences.getTileErrorState(); if (errorState == AppPreferences.TILE_ERROR_SHIZUKU) { tile.setState(Tile.STATE_UNAVAILABLE); - tile.setLabel("Shizuku Unavailable"); - tile.setIcon(TileIconManager.getCachedIcon("?")); + tile.setLabel(getString(R.string.tile_shizuku_unavailable)); + tile.setIcon(TileIconManager.getCachedIcon("?", "", false)); tile.updateTile(); return; } else if (errorState == AppPreferences.TILE_ERROR_ROOT) { tile.setState(Tile.STATE_UNAVAILABLE); - tile.setLabel("Root Unavailable"); - tile.setIcon(TileIconManager.getCachedIcon("?")); + tile.setLabel(getString(R.string.tile_root_unavailable)); + tile.setIcon(TileIconManager.getCachedIcon("?", "", false)); tile.updateTile(); return; } else if (errorState == AppPreferences.TILE_ERROR_CMD) { tile.setState(Tile.STATE_INACTIVE); - tile.setLabel("Error Check App"); - tile.setIcon(TileIconManager.getCachedIcon("?")); + tile.setLabel(getString(R.string.tile_error_check_app)); + tile.setIcon(TileIconManager.getCachedIcon("?", "", false)); tile.updateTile(); return; } @@ -255,19 +277,36 @@ private void updateTileUI(NetworkMode mode) { if (mode == NetworkMode.UNKNOWN) { if (appPreferences.getExecutionMode() == ExecutionMode.NONE) { tile.setState(Tile.STATE_UNAVAILABLE); - tile.setLabel("Setup Required"); + tile.setLabel(getString(R.string.tile_setup_required)); } else { NetworkMode firstMode = tileCycleManager.getFirstMode(); tile.setState(Tile.STATE_INACTIVE); - tile.setLabel("Tap to Set " + firstMode.getTileLabel()); + tile.setLabel(getString(R.string.tile_tap_to_set, firstMode.getTileLabel())); } - tile.setIcon(TileIconManager.getCachedIcon("?")); + tile.setIcon(TileIconManager.getCachedIcon("?", "", false)); } else { tile.setState(Tile.STATE_ACTIVE); tile.setLabel(mode.getTileLabel()); - tile.setIcon(TileIconManager.getCachedIcon(mode.getIconText())); + + // Determine badge and auto state + com.dhangofa.networktoggle.model.TargetSim targetSim = appPreferences.getTargetSim(); + String badge = ""; + boolean isAuto = targetSim == com.dhangofa.networktoggle.model.TargetSim.AUTO; + + if (isAuto) { + int activeSlot = simResolver.resolveTargetSlotIndex(appPreferences.getExecutionMode()); + badge = String.valueOf(activeSlot + 1); + } else if (targetSim == com.dhangofa.networktoggle.model.TargetSim.SIM_1) { + badge = "1"; + } else if (targetSim == com.dhangofa.networktoggle.model.TargetSim.SIM_2) { + badge = "2"; + } else if (targetSim == com.dhangofa.networktoggle.model.TargetSim.BOTH) { // Future proofing for BOTH + badge = "B"; + } + + tile.setIcon(TileIconManager.getCachedIcon(mode.getIconText(), badge, isAuto)); } tile.updateTile(); @@ -278,8 +317,7 @@ private void updateTileUI(NetworkMode mode) { private void showAutoSimErrorToast() { Toast.makeText( this, - "Unable to detect active data SIM automatically. " - + "Please choose SIM from the app.", + getString(R.string.toast_auto_sim_failed), Toast.LENGTH_LONG ).show(); } From b9bb1eb3b20680fab82f87f3b0ddbadca72b2446 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:08:46 +0530 Subject: [PATCH 057/394] Added activity for executing app shortcuts - Created invisible activity to handle app shortcut intents - Handled network mode switching from home screen/OS routines --- .../networktoggle/ShortcutActionActivity.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/ShortcutActionActivity.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/ShortcutActionActivity.java b/app/src/main/java/com/dhangofa/networktoggle/ShortcutActionActivity.java new file mode 100644 index 0000000..18f596d --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/ShortcutActionActivity.java @@ -0,0 +1,38 @@ +package com.dhangofa.networktoggle; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.widget.Toast; + +public class ShortcutActionActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + if (intent != null) { + String mode = intent.getStringExtra("mode"); + int sim = intent.getIntExtra("sim", -1); + + if (mode != null) { + Intent broadcastIntent = new Intent(this, AutomationReceiver.class); + broadcastIntent.setAction("com.dhangofa.networktoggle.SET_MODE"); + broadcastIntent.putExtra("mode", mode); + broadcastIntent.putExtra("sim", sim); + sendBroadcast(broadcastIntent); + + String displayMode = mode.replace("_ONLY", " Only").replace("PREF_", "Pref "); + String target = ""; + if (sim == 1) target = " on SIM 1"; + else if (sim == 2) target = " on SIM 2"; + else if (sim == 3) target = " on Both SIMs"; + + Toast.makeText(getApplicationContext(), getString(R.string.toast_applying_mode, displayMode, target), Toast.LENGTH_SHORT).show(); + } + } + + finish(); + } +} From 372a72991ad1f239ae0e943cf4aa06463bd461bd Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:10:52 +0530 Subject: [PATCH 058/394] Added dual-SIM capability resolution - Added logic to combine network capabilities when TargetSim.BOTH is selected --- .../telephony/NetworkCapabilityResolver.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkCapabilityResolver.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkCapabilityResolver.java index 07200a7..a56e4a3 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkCapabilityResolver.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkCapabilityResolver.java @@ -36,6 +36,28 @@ public NetworkCapabilityResolver(AppPreferences appPreferences, SimResolver simR public NetworkCapabilities getCapabilities(ExecutionMode mode) { if (mode == ExecutionMode.NONE) return NetworkCapabilities.assumeAll(); + com.dhangofa.networktoggle.model.TargetSim targetSim = appPreferences.getTargetSim(); + if (targetSim == com.dhangofa.networktoggle.model.TargetSim.BOTH) { + simResolver.setOverrideTargetSim(com.dhangofa.networktoggle.model.TargetSim.SIM_1); + NetworkCapabilities caps1 = getSingleSimCapabilities(mode); + + simResolver.setOverrideTargetSim(com.dhangofa.networktoggle.model.TargetSim.SIM_2); + NetworkCapabilities caps2 = getSingleSimCapabilities(mode); + + simResolver.setOverrideTargetSim(null); + + return new NetworkCapabilities( + caps1.supports2g && caps2.supports2g, + caps1.supports3g && caps2.supports3g, + caps1.supports4g && caps2.supports4g, + caps1.supports5g && caps2.supports5g + ); + } + + return getSingleSimCapabilities(mode); + } + + private NetworkCapabilities getSingleSimCapabilities(ExecutionMode mode) { // ONE SINGLE CALL to get slotIndex, subId, and carrierName! SimResolver.SimInfo simInfo = simResolver.resolveTargetSimInfo(mode); From f92cf9e411bdf757a0b296b4ab890a04f63cc1d7 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:13:21 +0530 Subject: [PATCH 059/394] Added dual-SIM mode reading - Added logic to query network modes for both SIMs simultaneously --- .../telephony/NetworkModeReader.java | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java index f9b09d8..442ac07 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java @@ -1,36 +1,60 @@ package com.dhangofa.networktoggle.telephony; -/** - * Main entry point for reading the current active network mode. - * It routes the request to either the Root or Shizuku readers depending on user settings. - */ - +/** Main entry point for reading the current active network mode. */ import android.content.Context; import com.dhangofa.networktoggle.config.AppPreferences; import com.dhangofa.networktoggle.model.ExecutionMode; import com.dhangofa.networktoggle.model.NetworkMode; +import com.dhangofa.networktoggle.model.TargetSim; public final class NetworkModeReader { private final AppPreferences appPreferences; private final ShizukuBinderModeReader shizukuBinderReader; private final PrivilegedModeReader privilegedModeReader; + private final SimResolver simResolver; public NetworkModeReader(Context context, AppPreferences appPreferences, SimResolver simResolver) { this.appPreferences = appPreferences; this.shizukuBinderReader = new ShizukuBinderModeReader(simResolver); this.privilegedModeReader = new PrivilegedModeReader(context, simResolver); + this.simResolver = simResolver; } public NetworkMode readCurrentMode() { ExecutionMode executionMode = appPreferences.getExecutionMode(); if (executionMode == ExecutionMode.NONE) return NetworkMode.UNKNOWN; + + TargetSim originalTarget = appPreferences.getTargetSim(); + + if (originalTarget == TargetSim.BOTH) { + simResolver.setOverrideTargetSim(TargetSim.SIM_1); + NetworkMode mode1 = readSingleMode(executionMode, TargetSim.SIM_1); + + simResolver.setOverrideTargetSim(TargetSim.SIM_2); + NetworkMode mode2 = readSingleMode(executionMode, TargetSim.SIM_2); + + simResolver.setOverrideTargetSim(null); + + if (mode1 != NetworkMode.UNKNOWN && mode1 == mode2) { + return mode1; + } + return NetworkMode.UNKNOWN; + } + + return readSingleMode(executionMode, originalTarget); + } + + private NetworkMode readSingleMode(ExecutionMode executionMode, TargetSim originalTarget) { + NetworkMode mode = NetworkMode.UNKNOWN; // 1. Shizuku Fast-Path (Binder IPC) if (executionMode == ExecutionMode.SHIZUKU) { - NetworkMode mode = shizukuBinderReader.readCurrentMode(executionMode); - if (mode != NetworkMode.UNKNOWN) return mode; + mode = shizukuBinderReader.readCurrentMode(executionMode); } + // 2. Root/Shizuku privileged shell fallback path - return privilegedModeReader.readCurrentMode(executionMode, appPreferences.getTargetSim()); + if (mode == NetworkMode.UNKNOWN) { + mode = privilegedModeReader.readCurrentMode(executionMode, originalTarget); + } + return mode; } } - From 79837211d799bd9f80085d5cc433d3ed48f32339 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:14:49 +0530 Subject: [PATCH 060/394] Added target SIM override functionality - Added overrideTargetSim field and setter - Allowed overriding the target SIM temporarily for BOTH mode operations --- .../com/dhangofa/networktoggle/telephony/SimResolver.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java index b46a2d2..0ac57a4 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java @@ -28,6 +28,8 @@ public final class SimResolver { private final Context context; private final AppPreferences appPreferences; + private TargetSim overrideTargetSim = null; + // Data class to hold all extracted variables in one place public static class SimInfo { public final int subId; @@ -50,8 +52,12 @@ Context getContext() { return context; } + public void setOverrideTargetSim(TargetSim targetSim) { + this.overrideTargetSim = targetSim; + } + public SimInfo resolveTargetSimInfo(ExecutionMode executionMode) { - TargetSim targetSim = appPreferences.getTargetSim(); + TargetSim targetSim = overrideTargetSim != null ? overrideTargetSim : appPreferences.getTargetSim(); int targetSubId = INVALID_SUB_ID; int targetSlotIndex = INVALID_SLOT_INDEX; String carrierName = ""; From 8450685fa193bde0a9117aef8cb38aa69d29b20e Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:16:23 +0530 Subject: [PATCH 061/394] Added new carousel pages and localization - Replaced hardcoded strings with R.string references - Added App Shortcuts carousel page - Added Broadcast Automation carousel page --- .../networktoggle/ui/CarouselManager.java | 111 +++++++++++++----- 1 file changed, 83 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/CarouselManager.java b/app/src/main/java/com/dhangofa/networktoggle/ui/CarouselManager.java index 0ecd832..9bc891b 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/ui/CarouselManager.java +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/CarouselManager.java @@ -61,6 +61,18 @@ public CarouselItem(String title, String desc, String btnText, String url, int i private float touchStartX = 0f; private boolean isFirstCarouselRender = true; + + private View[] multiCards; + private TextView[] multiTitles; + private TextView[] multiDescs; + private View[] multiIconContainers; + private ImageView[] multiIcons; + private View[] multiButtons; + private TextView[] multiBtnTexts; + private ImageView[] multiBtnIcons; + private boolean isMultiMode = false; + private View multiCarouselContainer; + public CarouselManager(Activity activity) { this.activity = activity; this.carouselBackground = activity.findViewById(R.id.carouselBackground); @@ -72,47 +84,90 @@ public CarouselManager(Activity activity) { this.carouselButton = activity.findViewById(R.id.carouselButton); this.carouselIconContainer = activity.findViewById(R.id.carouselIconContainer); this.carouselIndicators = activity.findViewById(R.id.carouselIndicators); + this.multiCarouselContainer = activity.findViewById(R.id.multiCarouselContainer); + if (this.multiCarouselContainer != null) { + isMultiMode = true; + multiCards = new View[]{activity.findViewById(R.id.staticCard1), activity.findViewById(R.id.staticCard2), activity.findViewById(R.id.staticCard3)}; + multiTitles = new TextView[]{activity.findViewById(R.id.staticTitle1), activity.findViewById(R.id.staticTitle2), activity.findViewById(R.id.staticTitle3)}; + multiDescs = new TextView[]{activity.findViewById(R.id.staticDesc1), activity.findViewById(R.id.staticDesc2), activity.findViewById(R.id.staticDesc3)}; + multiIconContainers = new View[]{activity.findViewById(R.id.staticIconContainer1), activity.findViewById(R.id.staticIconContainer2), activity.findViewById(R.id.staticIconContainer3)}; + multiIcons = new ImageView[]{activity.findViewById(R.id.staticIcon1), activity.findViewById(R.id.staticIcon2), activity.findViewById(R.id.staticIcon3)}; + multiButtons = new View[]{activity.findViewById(R.id.staticCardButton1), activity.findViewById(R.id.staticCardButton2), activity.findViewById(R.id.staticCardButton3)}; + multiBtnTexts = new TextView[]{activity.findViewById(R.id.staticBtnText1), activity.findViewById(R.id.staticBtnText2), activity.findViewById(R.id.staticBtnText3)}; + multiBtnIcons = new ImageView[]{activity.findViewById(R.id.staticBtnIcon1), activity.findViewById(R.id.staticBtnIcon2), activity.findViewById(R.id.staticBtnIcon3)}; + } + } public void setupCarousel(boolean isUIAuthorized) { - if (carouselBackground == null) return; + if (carouselBackground == null && !isMultiMode) return; carouselItems = new CarouselItem[]{ - new CarouselItem("New to NetToggle?", "Learn how to set up Execution Modes", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/1.-Execution-Mode-Configuration", R.drawable.ic_terminal, R.color.first_pg_bg, R.color.exec_accent, R.color.view_guide_button_bg), - new CarouselItem("Target SIM & Cycle", "Learn how to configure your modes", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/2.-Target-SIM-Setup-&-Quick-Tile-Cycle-Guide", R.drawable.ic_sim_card, R.color.second_pg_bg, R.color.accent_orange, R.color.view_guide_button_bg), - new CarouselItem("Quick Settings Ready", "Add the tile to your Control Center", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/3.-Adding-the-Tile-to-Quick-Settings", R.drawable.ic_network_bars, R.color.third_pg_bg, R.color.accent_pink, R.color.view_guide_button_bg) + new CarouselItem(activity.getString(R.string.carousel_title_1), activity.getString(R.string.carousel_desc_1), activity.getString(R.string.carousel_btn_1), "https://github.com/Dhangofa/NetToggle/wiki/1.-Execution-Mode-Configuration", R.drawable.ic_terminal, R.color.carousel_page1_bg, R.color.carousel_page1_accent, R.color.carousel_button_bg), + new CarouselItem(activity.getString(R.string.carousel_title_2), activity.getString(R.string.carousel_desc_2), activity.getString(R.string.carousel_btn_1), "https://github.com/Dhangofa/NetToggle/wiki/2.-Target-SIM-Setup-&-Quick-Tile-Cycle-Guide", R.drawable.ic_sim_card, R.color.carousel_page2_bg, R.color.carousel_page2_accent, R.color.carousel_button_bg), + new CarouselItem(activity.getString(R.string.carousel_title_3), activity.getString(R.string.carousel_desc_3), activity.getString(R.string.carousel_btn_1), "https://github.com/Dhangofa/NetToggle/wiki/3.-Adding-the-Tile-to-Quick-Settings", R.drawable.ic_network_bars, R.color.carousel_page3_bg, R.color.carousel_page3_accent, R.color.carousel_button_bg), + new CarouselItem(activity.getString(R.string.carousel_title_4), activity.getString(R.string.carousel_desc_4), activity.getString(R.string.carousel_btn_1), "https://github.com/Dhangofa/NetToggle/wiki/4.-App-Shortcuts-&-Built%E2%80%90in-OS-Routines", R.drawable.ic_shortcut_network, R.color.carousel_page4_bg, R.color.carousel_page4_accent, R.color.carousel_button_bg), + new CarouselItem(activity.getString(R.string.carousel_title_5), activity.getString(R.string.carousel_desc_5), activity.getString(R.string.carousel_btn_1), "https://github.com/Dhangofa/NetToggle/wiki/5.-Broadcast-Automation-(Tasker,-MacroDroid,-Automate)", R.drawable.ic_magic_wand, R.color.carousel_page5_bg, R.color.carousel_page5_accent, R.color.carousel_button_bg) }; setupCarouselIndicators(carouselItems.length); renderCarouselPage(isUIAuthorized ? 1 : 0, false, 0); - carouselBackground.setOnTouchListener((v, event) -> { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - touchStartX = event.getX(); - return true; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - float deltaX = event.getX() - touchStartX; - if (Math.abs(deltaX) > 100) { - if (deltaX > 0) { - currentCarouselIndex = (currentCarouselIndex - 1 + carouselItems.length) % carouselItems.length; - renderCarouselPage(currentCarouselIndex, true, -1); - } else if (deltaX < 0) { - currentCarouselIndex = (currentCarouselIndex + 1) % carouselItems.length; - renderCarouselPage(currentCarouselIndex, true, 1); + View swipeTarget = isMultiMode ? multiCarouselContainer : carouselBackground; + if (swipeTarget != null) { + swipeTarget.setOnTouchListener((v, event) -> { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + touchStartX = event.getX(); + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + float deltaX = event.getX() - touchStartX; + if (Math.abs(deltaX) > 100) { + if (deltaX > 0) { + currentCarouselIndex = (currentCarouselIndex - 1 + carouselItems.length) % carouselItems.length; + renderCarouselPage(currentCarouselIndex, true, -1); + } else if (deltaX < 0) { + currentCarouselIndex = (currentCarouselIndex + 1) % carouselItems.length; + renderCarouselPage(currentCarouselIndex, true, 1); + } + } else if (event.getAction() == MotionEvent.ACTION_UP) { + v.performClick(); } - } else if (event.getAction() == MotionEvent.ACTION_UP) { - v.performClick(); - } - return true; - } - return false; - }); - - carouselBackground.setOnClickListener(v -> {}); + return true; + } + return false; + }); + swipeTarget.setOnClickListener(v -> {}); + } } private void renderCarouselPage(int index, boolean animate, int direction) { + if (isMultiMode) { + for (int i = 0; i < 3; i++) { + int itemIndex = (index + i) % carouselItems.length; + CarouselItem item = carouselItems[itemIndex]; + + multiTitles[i].setText(item.title); + multiDescs[i].setText(item.desc); + multiBtnTexts[i].setText(item.btnText); + multiBtnTexts[i].setTextColor(activity.getColor(item.accentColor)); + + multiIcons[i].setImageResource(item.iconRes); + multiIcons[i].setColorFilter(activity.getColor(item.accentColor)); + multiBtnIcons[i].setColorFilter(activity.getColor(item.accentColor)); + + multiCards[i].setBackgroundTintList(android.content.res.ColorStateList.valueOf(activity.getColor(item.bgColor))); + multiButtons[i].setBackgroundTintList(android.content.res.ColorStateList.valueOf(activity.getColor(item.btnBgColor))); + multiIconContainers[i].setBackgroundTintList(android.content.res.ColorStateList.valueOf(activity.getColor(item.btnBgColor))); + + multiButtons[i].setOnClickListener(v -> { + try { activity.startActivity(new android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(item.url))); } catch (Exception e) {} + }); + } + updateCarouselIndicators((index + 1) % carouselItems.length); + return; + } + CarouselItem item = carouselItems[index]; Runnable updateViews = () -> { carouselTitle.setText(item.title); From b8f278c3cb236fbbc200d77c035e9648a04d0410 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:17:50 +0530 Subject: [PATCH 062/394] Updated dialogs for localization - Passed Activity context to DiagnosticReporter for string localization --- .../main/java/com/dhangofa/networktoggle/ui/DialogHelper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/DialogHelper.java b/app/src/main/java/com/dhangofa/networktoggle/ui/DialogHelper.java index a451e7d..e5a441f 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/ui/DialogHelper.java +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/DialogHelper.java @@ -57,7 +57,7 @@ public static Dialog buildDiagnosticDialog(Activity activity, AppPreferences app TextView reportText = dialog.findViewById(R.id.diagnosticReportText); if (reportText != null) { - String report = DiagnosticReporter.generateReport(appPreferences, simResolver); + String report = DiagnosticReporter.generateReport(activity, appPreferences, simResolver); reportText.setText(report); } @@ -75,7 +75,7 @@ public static Dialog buildDiagnosticDialog(Activity activity, AppPreferences app ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Activity.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("Diagnostic Report", reportText.getText()); clipboard.setPrimaryClip(clip); - Toast.makeText(activity, "Report copied to clipboard", Toast.LENGTH_SHORT).show(); + Toast.makeText(activity, activity.getString(R.string.toast_report_copied), Toast.LENGTH_SHORT).show(); }); } From f29f8032ba2ab60e18353c509b047da788d1d7b8 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:19:14 +0530 Subject: [PATCH 063/394] Updated status messaging logic - Replaced hardcoded status colors with integer codes (1=Success, 2=Error) for robust UI updates --- .../ui/ExecutionStateController.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/ExecutionStateController.java b/app/src/main/java/com/dhangofa/networktoggle/ui/ExecutionStateController.java index ff90173..b97402e 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/ui/ExecutionStateController.java +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/ExecutionStateController.java @@ -51,7 +51,7 @@ public ExecutionStateController(Activity activity, AppPreferences appPreferences activity.runOnUiThread(() -> { if (!isDestroyed && appPreferences != null && appPreferences.getExecutionMode() == ExecutionMode.SHIZUKU) { - statusCallback.onStatusUpdate("Shizuku is not running.", 0xFFFF5555); + statusCallback.onStatusUpdate("Shizuku is not running.", 2); } }); @@ -90,7 +90,7 @@ public void destroy() { } public void checkRootPermission() { - statusCallback.onStatusUpdate("Checking root permission...", 0xFFFFB300); + statusCallback.onStatusUpdate("Checking root permission...", 3); rootCheckThread = new Thread(() -> { boolean granted = false; Process process = null; @@ -110,10 +110,10 @@ public void checkRootPermission() { if (isDestroyed || appPreferences == null || appPreferences.getExecutionMode() != ExecutionMode.ROOT) return; if (finalGranted) { - statusCallback.onStatusUpdate("Root mode active & authorized!", 0xFF1B873F); + statusCallback.onStatusUpdate("Root mode active & authorized!", 1); appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_NONE); } else { - statusCallback.onStatusUpdate("Root permission denied or unavailable.", 0xFFFF5555); + statusCallback.onStatusUpdate("Root permission denied or unavailable.", 2); appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_ROOT); } TileService.requestListeningState(activity, new ComponentName(activity, NetworkTileService.class)); @@ -126,7 +126,7 @@ public void checkShizukuPermission(boolean requestIfNeeded) { if (isDestroyed) return; try { if (!Shizuku.pingBinder()) { - statusCallback.onStatusUpdate("Shizuku is not running.", 0xFFFF5555); + statusCallback.onStatusUpdate("Shizuku is not running.", 2); if (appPreferences != null) { appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_SHIZUKU); TileService.requestListeningState(activity, new ComponentName(activity, NetworkTileService.class)); @@ -134,24 +134,24 @@ public void checkShizukuPermission(boolean requestIfNeeded) { return; } if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { - statusCallback.onStatusUpdate("Shizuku mode active & authorized!", 0xFF1B873F); + statusCallback.onStatusUpdate("Shizuku mode active & authorized!", 1); if (appPreferences != null) { appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_NONE); TileService.requestListeningState(activity, new ComponentName(activity, NetworkTileService.class)); } return; } - statusCallback.onStatusUpdate("Shizuku permission not granted.", 0xFFFFB300); + statusCallback.onStatusUpdate("Shizuku permission not granted.", 2); if (appPreferences != null) { appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_SHIZUKU); TileService.requestListeningState(activity, new ComponentName(activity, NetworkTileService.class)); } if (requestIfNeeded) { - statusCallback.onStatusUpdate("Requesting Shizuku permission...", 0xFFFFB300); + statusCallback.onStatusUpdate("Requesting Shizuku permission...", 3); Shizuku.requestPermission(0); } } catch (Exception e) { - statusCallback.onStatusUpdate("Shizuku check failed.", 0xFFFF5555); + statusCallback.onStatusUpdate("Shizuku check failed.", 2); if (appPreferences != null) { appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_SHIZUKU); TileService.requestListeningState(activity, new ComponentName(activity, NetworkTileService.class)); From 41e92f0a818f3f361d17a3bafe3e6e1544fa802d Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:20:33 +0530 Subject: [PATCH 064/394] Added UI helper for routine shortcuts - Created dialog helper to manage creation and deletion of app shortcuts --- .../ui/ShortcutDialogHelper.java | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 app/src/main/java/com/dhangofa/networktoggle/ui/ShortcutDialogHelper.java diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/ShortcutDialogHelper.java b/app/src/main/java/com/dhangofa/networktoggle/ui/ShortcutDialogHelper.java new file mode 100644 index 0000000..35678a3 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/ShortcutDialogHelper.java @@ -0,0 +1,292 @@ +package com.dhangofa.networktoggle.ui; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.graphics.drawable.Icon; +import android.os.Build; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.Toast; + +import com.dhangofa.networktoggle.R; +import com.dhangofa.networktoggle.ShortcutActionActivity; +import com.dhangofa.networktoggle.config.AppPreferences; + + +import java.util.ArrayList; +import java.util.List; + +public class ShortcutDialogHelper { + + public static void showDialog(Activity activity, AppPreferences prefs) { + android.app.Dialog dialog = new android.app.Dialog(activity, R.style.TransparentBottomSheetStyle); + View view = LayoutInflater.from(activity).inflate(R.layout.dialog_routine_shortcuts, null); + dialog.setContentView(view); + android.view.Window window = dialog.getWindow(); + if (window != null) { + window.setLayout(android.view.ViewGroup.LayoutParams.MATCH_PARENT, android.view.ViewGroup.LayoutParams.WRAP_CONTENT); + window.setGravity(android.view.Gravity.BOTTOM); + } + + LinearLayout container = view.findViewById(R.id.shortcutsContainer); + View btnAdd = view.findViewById(R.id.btnAddShortcut); + View btnSave = view.findViewById(R.id.btnSaveShortcuts); + View btnCancel = view.findViewById(R.id.btnCancel); + btnCancel.setOnClickListener(v -> dialog.dismiss()); + + + LayoutInflater inflater = LayoutInflater.from(activity); + List rows = new ArrayList<>(); + + boolean hasSim1 = false; + boolean hasSim2 = false; + + if (activity.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) { + SubscriptionManager sm = activity.getSystemService(SubscriptionManager.class); + if (sm != null) { + try { + List infos = sm.getActiveSubscriptionInfoList(); + if (infos != null) { + for (SubscriptionInfo info : infos) { + if (info.getSimSlotIndex() == 0) hasSim1 = true; + if (info.getSimSlotIndex() == 1) hasSim2 = true; + } + } + } catch (Exception ignored) {} + } + } else { + // Assume both if no permission + hasSim1 = true; + hasSim2 = true; + } + + // Failsafe in case OS returned empty list incorrectly but capabilities exist + if (!hasSim1 && !hasSim2) { + hasSim1 = true; + } + + List simDisplayList = new ArrayList<>(); + List simValueList = new ArrayList<>(); + + if (hasSim1) { + simDisplayList.add("SIM 1"); + simValueList.add(1); + } + if (hasSim2) { + simDisplayList.add("SIM 2"); + simValueList.add(2); + } + if (hasSim1 && hasSim2) { + simDisplayList.add("Both"); + simValueList.add(3); + } + + AppPreferences.NetworkCapabilities deviceCaps = prefs.getDeviceCapabilities(); + if (deviceCaps == null) deviceCaps = AppPreferences.NetworkCapabilities.assumeAll(); + AppPreferences.NetworkCapabilities sim1Caps = prefs.getSlotCapabilities(0); + if (sim1Caps == null) sim1Caps = deviceCaps; + AppPreferences.NetworkCapabilities sim2Caps = prefs.getSlotCapabilities(1); + if (sim2Caps == null) sim2Caps = deviceCaps; + + // BOTH capabilities should be the intersection (lowest common denominator) of SIM 1 and SIM 2 + AppPreferences.NetworkCapabilities bothCaps = new AppPreferences.NetworkCapabilities( + sim1Caps.supports2g && sim2Caps.supports2g, + sim1Caps.supports3g && sim2Caps.supports3g, + sim1Caps.supports4g && sim2Caps.supports4g, + sim1Caps.supports5g && sim2Caps.supports5g + ); + + AppPreferences.NetworkCapabilities finalSim1Caps = sim1Caps; + AppPreferences.NetworkCapabilities finalSim2Caps = sim2Caps; + AppPreferences.NetworkCapabilities finalBothCaps = bothCaps; + + Runnable updateAddButton = () -> { + boolean canAdd = rows.size() < 4; + btnAdd.setAlpha(canAdd ? 1f : 0.5f); + btnAdd.setEnabled(canAdd); + + }; + + Runnable addRow = new Runnable() { + public void run() { + if (rows.size() >= 4) return; + + View row = inflater.inflate(R.layout.item_routine_shortcut, container, false); + Spinner spinnerSim = row.findViewById(R.id.spinnerSim); + Spinner spinnerMode = row.findViewById(R.id.spinnerMode); + ImageView btnRemove = row.findViewById(R.id.btnRemoveShortcut); + + ArrayAdapter simAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_dropdown_item, simDisplayList); + spinnerSim.setAdapter(simAdapter); + + spinnerSim.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + int simValue = simValueList.get(position); + AppPreferences.NetworkCapabilities caps; + if (simValue == 1) caps = finalSim1Caps; + else if (simValue == 2) caps = finalSim2Caps; + else caps = finalBothCaps; + + List validModes = new ArrayList<>(); + List validDisplays = new ArrayList<>(); + + if (caps.supports5g) { + validModes.add("5G_ONLY"); validDisplays.add("5G Only"); + validModes.add("PREF_5G"); validDisplays.add("Pref 5G"); + } + if (caps.supports4g) { + validModes.add("4G_ONLY"); validDisplays.add("4G Only"); + validModes.add("PREF_4G"); validDisplays.add("Pref 4G"); + } + if (caps.supports3g) { + validModes.add("PREF_3G"); validDisplays.add("Pref 3G"); + } + if (caps.supports2g) { + validModes.add("2G_ONLY"); validDisplays.add("2G Only"); + } + + row.setTag(validModes); + + ArrayAdapter modeAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_dropdown_item, validDisplays); + spinnerMode.setAdapter(modeAdapter); + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); + + btnRemove.setOnClickListener(v -> { + container.removeView(row); + rows.remove(row); + updateAddButton.run(); + }); + + rows.add(row); + container.addView(row); + updateAddButton.run(); + } + }; + + btnAdd.setOnClickListener(v -> addRow.run()); + + // Load existing + for (int i = 1; i <= 4; i++) { + String savedMode = prefs.getRoutineShortcutMode(i); + if (!"NONE".equals(savedMode)) { + int savedSim = prefs.getRoutineShortcutSim(i); // 1=SIM1, 2=SIM2, 3=Both + + int mappedIndex = simValueList.indexOf(savedSim); + if (mappedIndex == -1) { + // Sim no longer available. Still load it so they can see and delete/change it, + // but default to the first available SIM. + mappedIndex = 0; + } + + addRow.run(); + View row = rows.get(rows.size() - 1); + Spinner spinnerSim = row.findViewById(R.id.spinnerSim); + + spinnerSim.setSelection(mappedIndex); + + // Hacky delay to let the spinner selection listener fire and populate the mode adapter + final String modeToSelect = savedMode; + row.postDelayed(() -> { + Spinner sm = row.findViewById(R.id.spinnerMode); + List vModes = (List) row.getTag(); + if (vModes != null) { + for (int k = 0; k < vModes.size(); k++) { + if (vModes.get(k).equals(modeToSelect)) { + sm.setSelection(k); + break; + } + } + } + }, 50); + } + } + + btnSave.setOnClickListener(v -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + ShortcutManager shortcutManager = activity.getSystemService(ShortcutManager.class); + if (shortcutManager != null) { + List shortcuts = new ArrayList<>(); + List seenCombos = new ArrayList<>(); + + int slot = 1; + for (View row : rows) { + Spinner spinnerSim = row.findViewById(R.id.spinnerSim); + Spinner spinnerMode = row.findViewById(R.id.spinnerMode); + + int simSelection = spinnerSim.getSelectedItemPosition(); + int simValue = simValueList.get(simSelection); + + List vModes = (List) row.getTag(); + if (vModes == null || vModes.isEmpty()) continue; + + int modeSelection = spinnerMode.getSelectedItemPosition(); + if (modeSelection < 0 || modeSelection >= vModes.size()) modeSelection = 0; + + String selectedMode = vModes.get(modeSelection); + String comboKey = simValue + "_" + selectedMode; + + if (seenCombos.contains(comboKey)) { + Toast.makeText(activity, activity.getString(R.string.toast_skipped_duplicate_shortcut), Toast.LENGTH_SHORT).show(); + continue; + } + seenCombos.add(comboKey); + + prefs.saveRoutineShortcut(slot, selectedMode, simValue); + + String simLabel = simDisplayList.get(simSelection); + String modeLabel = spinnerMode.getSelectedItem().toString(); + String shortLabel = (simValue == 3 ? "" : simLabel + " ") + modeLabel; + + Intent intent = new Intent(activity, ShortcutActionActivity.class); + intent.setAction("com.dhangofa.networktoggle.SHORTCUT_ACTION_" + slot); + intent.putExtra("mode", selectedMode); + intent.putExtra("sim", simValue); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + ShortcutInfo shortcut = new ShortcutInfo.Builder(activity, "routine_slot_" + slot) + .setShortLabel(shortLabel) + .setLongLabel("Switch to " + modeLabel + " (" + simLabel + ")") + .setIcon(Icon.createWithResource(activity, R.drawable.ic_shortcut_network)) + .setIntent(intent) + .build(); + + shortcuts.add(shortcut); + slot++; + } + + for (int i = slot; i <= 4; i++) { + prefs.saveRoutineShortcut(i, "NONE", 1); + } + + try { + shortcutManager.setDynamicShortcuts(shortcuts); + Toast.makeText(activity, activity.getString(R.string.toast_shortcuts_updated), Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + Toast.makeText(activity, activity.getString(R.string.toast_shortcuts_failed), Toast.LENGTH_SHORT).show(); + } + } + } else { + Toast.makeText(activity, activity.getString(R.string.toast_shortcuts_require_7_1), Toast.LENGTH_SHORT).show(); + } + dialog.dismiss(); + }); + + dialog.show(); + } +} From 686eaf993f52e1053fdf0820cb402c2009c2cdc4 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:21:43 +0530 Subject: [PATCH 065/394] Refactored Target SIM UI - Replaced legacy radio buttons with segmented container logic - Added UI support for TargetSim.BOTH - Localized strings --- .../ui/TargetSimUiController.java | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/TargetSimUiController.java b/app/src/main/java/com/dhangofa/networktoggle/ui/TargetSimUiController.java index d93527d..4ae0a99 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/ui/TargetSimUiController.java +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/TargetSimUiController.java @@ -28,9 +28,11 @@ public final class TargetSimUiController { private final RadioButton auto; private final RadioButton sim1; private final RadioButton sim2; + private final RadioButton simBoth; private final TextView warning; private final View sep1; private final View sep2; + private final View sep3; private boolean updating; private boolean authorized; @@ -44,9 +46,11 @@ public TargetSimUiController(Activity activity, AppPreferences prefs, Runnable c this.auto = activity.findViewById(R.id.radioSimAuto); this.sim1 = activity.findViewById(R.id.radioSim1); this.sim2 = activity.findViewById(R.id.radioSim2); + this.simBoth = activity.findViewById(R.id.radioSimBoth); this.warning = activity.findViewById(R.id.autoSimWarningText); this.sep1 = activity.findViewById(R.id.separatorAutoSim1); this.sep2 = activity.findViewById(R.id.separatorSim1Sim2); + this.sep3 = activity.findViewById(R.id.separatorSim2Both); } public void initialize() { @@ -56,13 +60,15 @@ public void initialize() { sim1.setChecked(true); } else if (targetSim == TargetSim.SIM_2) { sim2.setChecked(true); + } else if (targetSim == TargetSim.BOTH) { + simBoth.setChecked(true); } else { auto.setChecked(true); } View.OnTouchListener lock = (v, e) -> { if (!authorized && e.getAction() == MotionEvent.ACTION_DOWN) { - Toast.makeText(activity, "Please authorize Root or Shizuku to configure toggles.", Toast.LENGTH_SHORT).show(); + Toast.makeText(activity, activity.getString(R.string.toast_auth_required), Toast.LENGTH_SHORT).show(); return true; } return false; @@ -71,6 +77,7 @@ public void initialize() { auto.setOnTouchListener(lock); sim1.setOnTouchListener(lock); sim2.setOnTouchListener(lock); + simBoth.setOnTouchListener(lock); group.setOnCheckedChangeListener((g, id) -> select(id)); updateSeparators(); @@ -85,10 +92,16 @@ private void select(int id) { targetSim = TargetSim.SIM_1; } else if (id == R.id.radioSim2) { targetSim = TargetSim.SIM_2; + } else if (id == R.id.radioSimBoth) { + targetSim = TargetSim.BOTH; } if (targetSim != TargetSim.AUTO && !exists(targetSim)) { - Toast.makeText(activity, "No SIM card found in slot " + (targetSim.getManualSlotIndex() + 1), Toast.LENGTH_SHORT).show(); + if (targetSim == TargetSim.BOTH) { + Toast.makeText(activity, activity.getString(R.string.toast_both_sims_required), Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(activity, activity.getString(targetSim.getManualSlotIndex() == 0 ? R.string.toast_no_sim_slot_1 : R.string.toast_no_sim_slot_2), Toast.LENGTH_SHORT).show(); + } updating = true; auto.setChecked(true); updating = false; @@ -118,6 +131,16 @@ private boolean exists(TargetSim target) { List infos = sm.getActiveSubscriptionInfoList(); if (infos == null) return false; + if (target == TargetSim.BOTH) { + boolean hasSim1 = false; + boolean hasSim2 = false; + for (SubscriptionInfo info : infos) { + if (info.getSimSlotIndex() == 0) hasSim1 = true; + if (info.getSimSlotIndex() == 1) hasSim2 = true; + } + return hasSim1 && hasSim2; + } + for (SubscriptionInfo info : infos) { if (info.getSimSlotIndex() == target.getManualSlotIndex()) { return true; @@ -132,14 +155,9 @@ private boolean exists(TargetSim target) { public void setAuthorized(boolean value) { this.authorized = value; float alpha = value ? 1f : 0.4f; - group.setAlpha(alpha); - - View header = activity.findViewById(R.id.targetSimHeaderContainer); - if (header != null) { - header.setAlpha(alpha); - } - if (warning != null) { - warning.setAlpha(alpha); + View card = activity.findViewById(R.id.cardTargetSim); + if (card != null) { + card.setAlpha(alpha); } } @@ -150,8 +168,8 @@ public void updateAutoSimWarning() { warning.setVisibility(show ? View.VISIBLE : View.GONE); if (show) { - warning.setText("Auto SIM detection failed. Please choose SIM 1 or SIM 2 manually."); - warning.setTextColor(0xFFFF5555); + warning.setText(R.string.warning_auto_sim_failed); + warning.setTextColor(activity.getColor(R.color.status_error_text)); } } @@ -161,15 +179,27 @@ private void updateSeparators() { if (id == -1) { sep1.setVisibility(View.VISIBLE); sep2.setVisibility(View.VISIBLE); + sep3.setVisibility(View.VISIBLE); } else if (id == R.id.radioSimAuto) { sep1.setVisibility(View.INVISIBLE); sep2.setVisibility(View.VISIBLE); + sep3.setVisibility(View.VISIBLE); } else if (id == R.id.radioSim1) { sep1.setVisibility(View.INVISIBLE); sep2.setVisibility(View.INVISIBLE); - } else { + sep3.setVisibility(View.VISIBLE); + } else if (id == R.id.radioSim2) { sep1.setVisibility(View.VISIBLE); sep2.setVisibility(View.INVISIBLE); + sep3.setVisibility(View.INVISIBLE); + } else if (id == R.id.radioSimBoth) { + sep1.setVisibility(View.VISIBLE); + sep2.setVisibility(View.VISIBLE); + sep3.setVisibility(View.INVISIBLE); + } else { + sep1.setVisibility(View.VISIBLE); + sep2.setVisibility(View.VISIBLE); + sep3.setVisibility(View.VISIBLE); } } } From 76eb77f1363dc0261137b12f8ee02f5ee10557aa Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:22:59 +0530 Subject: [PATCH 066/394] Replace hardcoded strings with resource references - Replaced hardcoded mode labels with localized string resources - Updated cycle text formatting --- .../networktoggle/ui/TileCycleUiController.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java b/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java index 4b7ccb1..da3a7cb 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java @@ -147,13 +147,13 @@ private void handleSelection(NetworkMode mode, boolean selected) { String reason = ""; if ((mode == NetworkMode.PREFERRED_5G || mode == NetworkMode.FIVE_G_ONLY) && !currentCaps.supports5g) { supported = false; - reason = "5G is not supported by your device or current carrier."; + reason = activity.getString(R.string.tilecycle_5g_unsupported); } else if (mode == NetworkMode.PREFERRED_3G && !currentCaps.supports3g) { supported = false; - reason = "3G is disabled or not supported by your current carrier."; + reason = activity.getString(R.string.tilecycle_3g_unsupported); } else if (mode == NetworkMode.TWO_G_ONLY && !currentCaps.supports2g) { supported = false; - reason = "2G is disabled or not supported by your current carrier."; + reason = activity.getString(R.string.tilecycle_2g_unsupported); } if (!supported) { @@ -169,9 +169,9 @@ private void handleSelection(NetworkMode mode, boolean selected) { cycleChangedListener.onCycleChanged(cycleManager.getCycle()); } } else if (result == TileCycleManager.ChangeResult.MINIMUM_REACHED) { - showToast("Select at least 2 tile modes."); + showToast(activity.getString(R.string.toast_select_at_least_2)); } else if (result == TileCycleManager.ChangeResult.MAXIMUM_REACHED) { - showToast("You can select up to 3 tile modes."); + showToast(activity.getString(R.string.toast_select_up_to_3)); } refresh(); } @@ -193,13 +193,13 @@ private void refresh() { separatorCycleOnly1.setVisibility(mode5gOnly.isChecked() || mode4gOnly.isChecked() ? View.INVISIBLE : View.VISIBLE); separatorCycleOnly2.setVisibility(mode4gOnly.isChecked() || mode2gOnly.isChecked() ? View.INVISIBLE : View.VISIBLE); - selectedCount.setText("Selected: " + cycle.size() + "/3"); + selectedCount.setText(activity.getString(R.string.cycle_selected_count, cycle.size())); cycleOrder.setText(buildOrderText(cycle)); updatingUi = false; } private String buildOrderText(List cycle) { - StringBuilder text = new StringBuilder("Cycle: "); + StringBuilder text = new StringBuilder(activity.getString(R.string.cycle_prefix)); for (int i = 0; i < cycle.size(); i++) { if (i > 0) text.append(" โ†’ "); text.append(cycle.get(i).getDisplayName()); From 32fd3c2d79014475ecd8e66604b40a11c944231c Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:24:10 +0530 Subject: [PATCH 067/394] Added dynamic icon badging - Updated icon rendering to support dynamic SIM badges and auto states --- .../networktoggle/ui/TileIconManager.java | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/TileIconManager.java b/app/src/main/java/com/dhangofa/networktoggle/ui/TileIconManager.java index 151065c..ac61c88 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/ui/TileIconManager.java +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/TileIconManager.java @@ -12,19 +12,15 @@ import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.drawable.Icon; +import android.graphics.DashPathEffect; +import java.util.HashMap; public final class TileIconManager { - private static Icon icon4g; - private static Icon icon5g; - private static Icon iconP5g; - private static Icon iconP4g; - private static Icon iconP3g; - private static Icon icon2g; - private static Icon iconUnknown; + private static final HashMap iconCache = new HashMap<>(); private TileIconManager() {} - private static Icon createTextOnlyIcon(String text) { + private static Icon createTextOnlyIcon(String text, String badge, boolean isAuto) { int size = 256; Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); @@ -33,7 +29,7 @@ private static Icon createTextOnlyIcon(String text) { paint.setColor(Color.WHITE); paint.setTypeface(Typeface.create("sans-serif-condensed", Typeface.BOLD)); paint.setTextAlign(Paint.Align.CENTER); - paint.setTextSize(190f); + paint.setTextSize(175f); float width = paint.measureText(text); if (width > 240f) { @@ -42,34 +38,48 @@ private static Icon createTextOnlyIcon(String text) { Paint.FontMetrics metrics = paint.getFontMetrics(); float y = (size / 2f) - (metrics.descent + metrics.ascent) / 2f; - canvas.drawText(text, size / 2f, y, paint); + // Shift text slightly to the left and down to make room for the badge + canvas.drawText(text, (size / 2f) - 10f, y + 10f, paint); + + if (badge != null && !badge.isEmpty()) { + float cx = size - 35f; + float cy = 35f; + float radius = 28f; + + // Draw circle + Paint circlePaint = new Paint(); + circlePaint.setAntiAlias(true); + circlePaint.setColor(Color.WHITE); + circlePaint.setStyle(Paint.Style.STROKE); + circlePaint.setStrokeWidth(9f); + if (isAuto) { + // Circumference is ~176 (2 * pi * 28). To get 4 dashes with smaller gaps, dash+gap should be ~44. + circlePaint.setPathEffect(new DashPathEffect(new float[]{36f, 8f}, 0f)); + } + canvas.drawCircle(cx, cy, radius, circlePaint); + + // Draw badge text + Paint badgeTextPaint = new Paint(); + badgeTextPaint.setAntiAlias(true); + badgeTextPaint.setColor(Color.WHITE); + badgeTextPaint.setTypeface(Typeface.create("sans-serif-black", Typeface.BOLD)); + badgeTextPaint.setFakeBoldText(true); // Force extra thickness + badgeTextPaint.setTextAlign(Paint.Align.CENTER); + badgeTextPaint.setTextSize(45f); + + Paint.FontMetrics badgeMetrics = badgeTextPaint.getFontMetrics(); + float badgeY = cy - (badgeMetrics.descent + badgeMetrics.ascent) / 2f; + canvas.drawText(badge, cx, badgeY, badgeTextPaint); + } return Icon.createWithBitmap(bitmap); } - public static Icon getCachedIcon(String text) { - switch (text) { - case "4G": - if (icon4g == null) icon4g = createTextOnlyIcon("4G"); - return icon4g; - case "5G": - if (icon5g == null) icon5g = createTextOnlyIcon("5G"); - return icon5g; - case "P5G": - if (iconP5g == null) iconP5g = createTextOnlyIcon("P5G"); - return iconP5g; - case "P4G": - if (iconP4g == null) iconP4g = createTextOnlyIcon("P4G"); - return iconP4g; - case "P3G": - if (iconP3g == null) iconP3g = createTextOnlyIcon("P3G"); - return iconP3g; - case "2G": - if (icon2g == null) icon2g = createTextOnlyIcon("2G"); - return icon2g; - default: - if (iconUnknown == null) iconUnknown = createTextOnlyIcon("?"); - return iconUnknown; + public static Icon getCachedIcon(String text, String badge, boolean isAuto) { + String cacheKey = text + "_" + badge + "_" + isAuto; + if (!iconCache.containsKey(cacheKey)) { + iconCache.put(cacheKey, createTextOnlyIcon(text, badge, isAuto)); } + return iconCache.get(cacheKey); } } From 75af6198e95578360e178b42c1809c588c3fa88e Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:25:47 +0530 Subject: [PATCH 068/394] Localized diagnostic reports - Replaced hardcoded English report strings with localized resources - Modified DiagnosticReporter to fetch and append the app version name and code via PackageManager - Added active Shizuku Binder ping and permission status checks directly into the report\n- Generated the report payload even if no errors are currently recorded\n- Enhanced device metadata to include the Build.PRODUCT and formatted SDK versions nicely\n- Refactored `versionCode` logic to use `getLongVersionCode` on supported API levels --- .../util/DiagnosticReporter.java | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/dhangofa/networktoggle/util/DiagnosticReporter.java b/app/src/main/java/com/dhangofa/networktoggle/util/DiagnosticReporter.java index f3c0581..1a60efe 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/util/DiagnosticReporter.java +++ b/app/src/main/java/com/dhangofa/networktoggle/util/DiagnosticReporter.java @@ -1,11 +1,8 @@ package com.dhangofa.networktoggle.util; -/** - * Utility class to generate detailed crash logs and diagnostic strings. - * It grabs device info, Android versions, Shizuku state, and network capabilities so to easily - * debug issues when things go wrong. - */ - +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.os.Build; import com.dhangofa.networktoggle.config.AppPreferences; import com.dhangofa.networktoggle.model.DiagnosticError; @@ -13,51 +10,71 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; +import rikka.shizuku.Shizuku; public class DiagnosticReporter { - public static String generateReport(AppPreferences prefs, SimResolver simResolver) { - DiagnosticError error = prefs.getLastError(); - if (error == null) { - return "No recent errors recorded."; - } + public static String generateReport(Context context, AppPreferences prefs, SimResolver simResolver) { StringBuilder sb = new StringBuilder(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); sb.append("--- NETWORK TOGGLE DIAGNOSTIC REPORT ---\n"); - sb.append("Time: ").append(sdf.format(new Date(error.timestamp))).append("\n\n"); + sb.append("Generated: ").append(sdf.format(new Date())).append("\n\n"); sb.append("[DEVICE INFO]\n"); sb.append("Manufacturer: ").append(Build.MANUFACTURER).append("\n"); sb.append("Brand: ").append(Build.BRAND).append("\n"); sb.append("Device: ").append(Build.DEVICE).append("\n"); + sb.append("Product: ").append(Build.PRODUCT).append("\n"); sb.append("Model: ").append(Build.MODEL).append("\n"); - sb.append("Android Version: ").append(Build.VERSION.RELEASE).append("\n"); - sb.append("SDK Level: ").append(Build.VERSION.SDK_INT).append("\n\n"); + sb.append("Android Version: ").append(Build.VERSION.RELEASE) + .append(" (SDK ").append(Build.VERSION.SDK_INT).append(")\n\n"); sb.append("[APP STATE]\n"); + try { + PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + long versionCode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? pInfo.getLongVersionCode() : pInfo.versionCode; + sb.append("App Version: ").append(pInfo.versionName).append(" (").append(versionCode).append(")\n"); + } catch (Exception e) { + sb.append("App Version: Unknown\n"); + } sb.append("Execution Mode: ").append(prefs.getExecutionMode().name()).append("\n"); sb.append("Target SIM Setting: ").append(prefs.getTargetSim().name()).append("\n"); - int slotIndex = simResolver.resolveTargetSlotIndex(prefs.getExecutionMode()); sb.append("Resolved Slot Index: ").append(slotIndex).append("\n\n"); - sb.append("[ERROR DETAILS]\n"); - sb.append("Command Attempted:\n").append(error.command).append("\n\n"); - - sb.append("Exit Code: ").append(error.exitCode).append("\n\n"); - - if (error.exceptionMessage != null && !error.exceptionMessage.isEmpty()) { - sb.append("Exception Message:\n").append(error.exceptionMessage).append("\n\n"); + sb.append("[SHIZUKU STATUS]\n"); + try { + boolean isBinderAlive = Shizuku.pingBinder(); + sb.append("Binder Alive: ").append(isBinderAlive).append("\n"); + if (isBinderAlive) { + sb.append("Shizuku Version: ").append(Shizuku.getVersion()).append("\n"); + boolean hasPermission = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED; + sb.append("Permission Granted: ").append(hasPermission).append("\n"); + } + } catch (Throwable e) { + sb.append("Status: Error reading Shizuku status (").append(e.getClass().getSimpleName()).append(")\n"); } + sb.append("\n"); - if (error.stdout != null && !error.stdout.isEmpty()) { - sb.append("Standard Output (stdout):\n").append(error.stdout).append("\n\n"); + sb.append("[LAST ERROR EVENT]\n"); + DiagnosticError error = prefs.getLastError(); + if (error != null) { + sb.append("Time: ").append(sdf.format(new Date(error.timestamp))).append("\n\n"); + sb.append("Command Attempted:\n").append(error.command != null ? error.command : "N/A").append("\n\n"); + sb.append("Exit Code: ").append(error.exitCode).append("\n\n"); + if (error.exceptionMessage != null && !error.exceptionMessage.isEmpty()) { + sb.append("Exception Message:\n").append(error.exceptionMessage).append("\n\n"); + } + if (error.stdout != null && !error.stdout.isEmpty()) { + sb.append("Standard Output (stdout):\n").append(error.stdout).append("\n\n"); + } + sb.append("Standard Error (stderr):\n").append(error.stderr != null && !error.stderr.isEmpty() ? error.stderr : "(empty)").append("\n"); + } else { + sb.append("No recent errors recorded.\n"); } - sb.append("Standard Error (stderr):\n").append(error.stderr != null && !error.stderr.isEmpty() ? error.stderr : "(empty)").append("\n\n"); - - sb.append("--- END OF REPORT ---"); + sb.append("\n--- END OF REPORT ---"); return sb.toString(); } } From cc8281639fe36ecbe3eb8b85ca0a3b8693bcf60e Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:28:00 +0530 Subject: [PATCH 069/394] Update unselected color reference in XML selector - Updated color references to support Light/Dark themes dynamically --- app/src/main/res/color/color_segmented_text_cycle.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/color/color_segmented_text_cycle.xml b/app/src/main/res/color/color_segmented_text_cycle.xml index d9152f6..732eaec 100644 --- a/app/src/main/res/color/color_segmented_text_cycle.xml +++ b/app/src/main/res/color/color_segmented_text_cycle.xml @@ -1,5 +1,5 @@ - + From 6ef3d4b32b584c20ac28135498a5edee52df1c20 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:29:04 +0530 Subject: [PATCH 070/394] Update unselected color reference in XML selector - Updated color references to support dynamic theming --- app/src/main/res/color/color_segmented_text_exec.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/color/color_segmented_text_exec.xml b/app/src/main/res/color/color_segmented_text_exec.xml index 49c7c53..50324b9 100644 --- a/app/src/main/res/color/color_segmented_text_exec.xml +++ b/app/src/main/res/color/color_segmented_text_exec.xml @@ -1,5 +1,5 @@ - + From 7cd76f9f56a99945a5686143d70720ca22520073 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:30:27 +0530 Subject: [PATCH 071/394] Added target SIM text colors - Created color selector for Target SIM segmented buttons --- app/src/main/res/color/color_segmented_text_sim.xml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/src/main/res/color/color_segmented_text_sim.xml diff --git a/app/src/main/res/color/color_segmented_text_sim.xml b/app/src/main/res/color/color_segmented_text_sim.xml new file mode 100644 index 0000000..afd31d2 --- /dev/null +++ b/app/src/main/res/color/color_segmented_text_sim.xml @@ -0,0 +1,5 @@ + + + + + From 9ef90cdfc9354a21274c72e85a0fc6bfe3d64849 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:32:51 +0530 Subject: [PATCH 072/394] Update launcher background with gradient and rings - Adjusted background color for standard adaptive icon - Added dynamic sine-wave wavy rings --- .../res/drawable/ic_launcher_background.xml | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index da555f4..81d7e27 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -5,18 +5,35 @@ android:height="108dp" android:viewportWidth="108" android:viewportHeight="108"> + + - - - - + android:endY="108"> + + + + + + + + + + + + From f1de38514c7f4c369401bbeeda472e2ba38ff1d9 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:34:46 +0530 Subject: [PATCH 073/394] Refactor ic_launcher_foreground.xml for clarity - Updated vector paths for the main app logo - Increased stroke thickness of the central antenna --- .../res/drawable/ic_launcher_foreground.xml | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index c7c3207..9cd3582 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,28 +1,25 @@ + - - + - - - - - + + + - - - + + + From 4671cf0815c9391db31132a9500235216763a152 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:36:16 +0530 Subject: [PATCH 074/394] Add monochrome launcher icon as vector drawable - Created monochrome vector for Android 13+ themed icons --- .../res/drawable/ic_launcher_monochrome.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 app/src/main/res/drawable/ic_launcher_monochrome.xml diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 0000000..fe62a0c --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,18 @@ + + + + + + + From 47916660628b973d52f5227f76a2c5bdbc783cef Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:38:55 +0530 Subject: [PATCH 075/394] Updated legacy launcher icon - Updated the launcher icon with a new gradient background - Increased stroke thickness of the central antenna to match foreground --- app/src/main/res/drawable/ic_launcher.xml | 71 ++++++++++++++--------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml index e422163..657fe46 100644 --- a/app/src/main/res/drawable/ic_launcher.xml +++ b/app/src/main/res/drawable/ic_launcher.xml @@ -1,48 +1,63 @@ + - - - - - + + + - - - - + + + + - - + + + - - + + + + + + + + + android:fillColor="#400A1204" + android:pathData="M55,28 L53,28 A2,2 0 0,0 51,30 L51,42 L42,42 A2,2 0 0,0 40,44 L40,78 A2,2 0 0,0 42,80 L66,80 A2,2 0 0,0 68,78 L68,44 A2,2 0 0,0 66,42 L57,42 L57,30 A2,2 0 0,0 55,28 Z M47,49 L61,49 L61,73 L47,73 Z M32,46 A2,2 0 0,0 30,48 C30,58 34,68 40,75 A2,2 0 0,0 43,72 C37,66 34,58 34,48 A2,2 0 0,0 32,46 Z M76,46 A2,2 0 0,0 74,48 C74,58 71,66 65,72 A2,2 0 0,0 68,75 C74,68 78,58 78,48 A2,2 0 0,0 76,46 Z M23,36 A2,2 0 0,0 21,38 C21,52 26,65 35,76 A2,2 0 0,0 38,73 C29,63 25,51 25,38 A2,2 0 0,0 23,36 Z M85,36 A2,2 0 0,0 83,38 C83,51 79,63 70,73 A2,2 0 0,0 73,76 C82,65 87,52 87,38 A2,2 0 0,0 85,36 Z" /> - + + android:pathData="M55,28 L53,28 A2,2 0 0,0 51,30 L51,42 L42,42 A2,2 0 0,0 40,44 L40,78 A2,2 0 0,0 42,80 L66,80 A2,2 0 0,0 68,78 L68,44 A2,2 0 0,0 66,42 L57,42 L57,30 A2,2 0 0,0 55,28 Z M47,49 L61,49 L61,73 L47,73 Z M32,46 A2,2 0 0,0 30,48 C30,58 34,68 40,75 A2,2 0 0,0 43,72 C37,66 34,58 34,48 A2,2 0 0,0 32,46 Z M76,46 A2,2 0 0,0 74,48 C74,58 71,66 65,72 A2,2 0 0,0 68,75 C74,68 78,58 78,48 A2,2 0 0,0 76,46 Z M23,36 A2,2 0 0,0 21,38 C21,52 26,65 35,76 A2,2 0 0,0 38,73 C29,63 25,51 25,38 A2,2 0 0,0 23,36 Z M85,36 A2,2 0 0,0 83,38 C83,51 79,63 70,73 A2,2 0 0,0 73,76 C82,65 87,52 87,38 A2,2 0 0,0 85,36 Z" /> From af5a1b8479ae4fd6dc08eb790b306543ecab3816 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:41:26 +0530 Subject: [PATCH 076/394] Updated main UI logo - Updated vector paths for the UI logo asset - Increased stroke thickness of the central antenna to match launcher --- app/src/main/res/drawable/ic_logo_ui.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/drawable/ic_logo_ui.xml b/app/src/main/res/drawable/ic_logo_ui.xml index 852af25..489fa02 100644 --- a/app/src/main/res/drawable/ic_logo_ui.xml +++ b/app/src/main/res/drawable/ic_logo_ui.xml @@ -5,9 +5,9 @@ android:viewportWidth="108" android:viewportHeight="108"> From ccbf4f9a9a4f3f3586eaf5cbcf5d0b7e88267bf4 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:42:53 +0530 Subject: [PATCH 077/394] Add vector drawable for magic wand icon - Created magic wand vector for automation carousel page and FAB --- app/src/main/res/drawable/ic_magic_wand.xml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/src/main/res/drawable/ic_magic_wand.xml diff --git a/app/src/main/res/drawable/ic_magic_wand.xml b/app/src/main/res/drawable/ic_magic_wand.xml new file mode 100644 index 0000000..27e25e5 --- /dev/null +++ b/app/src/main/res/drawable/ic_magic_wand.xml @@ -0,0 +1,9 @@ + + + From 4f9b865dae4f91134e95131c4a78649e5893b527 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:44:32 +0530 Subject: [PATCH 078/394] Added new Quick Settings icon - Created specific simplified vector for the Quick Settings tile --- app/src/main/res/drawable/ic_qs_logo.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/src/main/res/drawable/ic_qs_logo.xml diff --git a/app/src/main/res/drawable/ic_qs_logo.xml b/app/src/main/res/drawable/ic_qs_logo.xml new file mode 100644 index 0000000..5e6459c --- /dev/null +++ b/app/src/main/res/drawable/ic_qs_logo.xml @@ -0,0 +1,12 @@ + + + + + From 72fd59dbce0f86d5709406fe3fd5aec43cd2d3d2 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:45:26 +0530 Subject: [PATCH 079/394] Delete app/src/main/res/drawable/ic_qs_network.xml - Deleted unused icon - Replaced with ic_qs_logo.xml --- app/src/main/res/drawable/ic_qs_network.xml | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_qs_network.xml diff --git a/app/src/main/res/drawable/ic_qs_network.xml b/app/src/main/res/drawable/ic_qs_network.xml deleted file mode 100644 index a5be7ca..0000000 --- a/app/src/main/res/drawable/ic_qs_network.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - From 4d7f32c040ae4b4be09ab358959eeb65392aeec7 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:51:58 +0530 Subject: [PATCH 080/394] Added app shortcut icon - Created vector icon for launcher shortcuts --- app/src/main/res/drawable/ic_shortcut_network.xml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/src/main/res/drawable/ic_shortcut_network.xml diff --git a/app/src/main/res/drawable/ic_shortcut_network.xml b/app/src/main/res/drawable/ic_shortcut_network.xml new file mode 100644 index 0000000..997b017 --- /dev/null +++ b/app/src/main/res/drawable/ic_shortcut_network.xml @@ -0,0 +1,9 @@ + + + From aef35079ee0e69fbf40e94eb15770cec7eaf32d9 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:53:45 +0530 Subject: [PATCH 081/394] Added AMOLED theme toggle icon - Created icon for AMOLED theme state --- app/src/main/res/drawable/ic_theme_amoled.xml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/src/main/res/drawable/ic_theme_amoled.xml diff --git a/app/src/main/res/drawable/ic_theme_amoled.xml b/app/src/main/res/drawable/ic_theme_amoled.xml new file mode 100644 index 0000000..ee0b4d6 --- /dev/null +++ b/app/src/main/res/drawable/ic_theme_amoled.xml @@ -0,0 +1,9 @@ + + + From ac9f96afa914d472e75c50d970f0d71ae706dfef Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:55:02 +0530 Subject: [PATCH 082/394] Added Dark theme toggle icon - Created icon for Dark theme state --- app/src/main/res/drawable/ic_theme_dark.xml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/src/main/res/drawable/ic_theme_dark.xml diff --git a/app/src/main/res/drawable/ic_theme_dark.xml b/app/src/main/res/drawable/ic_theme_dark.xml new file mode 100644 index 0000000..b46c30a --- /dev/null +++ b/app/src/main/res/drawable/ic_theme_dark.xml @@ -0,0 +1,9 @@ + + + From dbf8736942bf48b24476518688acdd879d1a581e Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:56:18 +0530 Subject: [PATCH 083/394] Added Light theme toggle icon - Created icon for Light theme state --- app/src/main/res/drawable/ic_theme_light.xml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/src/main/res/drawable/ic_theme_light.xml diff --git a/app/src/main/res/drawable/ic_theme_light.xml b/app/src/main/res/drawable/ic_theme_light.xml new file mode 100644 index 0000000..e1da761 --- /dev/null +++ b/app/src/main/res/drawable/ic_theme_light.xml @@ -0,0 +1,9 @@ + + + From e16c32cb2b33bf02bb9282300fff0ccf7be80d02 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:28:26 +0530 Subject: [PATCH 084/394] Updated cycle tab background - Updated the segmented tab selector to include a ripple effect and added a stroke for the selected item - Refactored selector to use updated semantic colors --- .../drawable/selector_segmented_tab_cycle.xml | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/drawable/selector_segmented_tab_cycle.xml b/app/src/main/res/drawable/selector_segmented_tab_cycle.xml index 89b5b3f..85cba2f 100644 --- a/app/src/main/res/drawable/selector_segmented_tab_cycle.xml +++ b/app/src/main/res/drawable/selector_segmented_tab_cycle.xml @@ -1,15 +1,20 @@ - - - - - - - + - - - - + + + + + + + + + + + + + + + - + From 490183e9415ff26d874b2ae78e0c75cdf53e1739 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:29:48 +0530 Subject: [PATCH 085/394] Updated execution tab background - Updated the drawable selector to include a ripple effect and added a stroke for the checked state - Refactored selector to use updated semantic colors --- .../drawable/selector_segmented_tab_exec.xml | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/drawable/selector_segmented_tab_exec.xml b/app/src/main/res/drawable/selector_segmented_tab_exec.xml index 28f41d0..bb79322 100644 --- a/app/src/main/res/drawable/selector_segmented_tab_exec.xml +++ b/app/src/main/res/drawable/selector_segmented_tab_exec.xml @@ -1,15 +1,20 @@ - - - - - - - + - - - - + + + + + + + + + + + + + + + - + From e8b293698812c72bfe5204a7d21c9ffc71a937fa Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:31:05 +0530 Subject: [PATCH 086/394] Added target SIM tab background - Created selector for target SIM segmented buttons - This XML file defines a ripple effect for a segmented tab with checked and unchecked states, including custom colors and corner radius --- .../drawable/selector_segmented_tab_sim.xml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 app/src/main/res/drawable/selector_segmented_tab_sim.xml diff --git a/app/src/main/res/drawable/selector_segmented_tab_sim.xml b/app/src/main/res/drawable/selector_segmented_tab_sim.xml new file mode 100644 index 0000000..00054d4 --- /dev/null +++ b/app/src/main/res/drawable/selector_segmented_tab_sim.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + From e020653a6c2c571362f4f33a77b210240ba1461b Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:36:58 +0530 Subject: [PATCH 087/394] Change shape to ripple effect for primary button - Refactored shape attributes for consistency --- app/src/main/res/drawable/shape_button_primary.xml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/drawable/shape_button_primary.xml b/app/src/main/res/drawable/shape_button_primary.xml index efa2239..aed6dca 100644 --- a/app/src/main/res/drawable/shape_button_primary.xml +++ b/app/src/main/res/drawable/shape_button_primary.xml @@ -1,5 +1,9 @@ - - - - + + + + + + + + From ddc67b941a7ee4dfe6c78b9e10aebf5b110adce7 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:38:15 +0530 Subject: [PATCH 088/394] Add default background shape for carousel - Created default rounded rectangle shape for carousel items --- .../main/res/drawable/shape_carousel_bg_default.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 app/src/main/res/drawable/shape_carousel_bg_default.xml diff --git a/app/src/main/res/drawable/shape_carousel_bg_default.xml b/app/src/main/res/drawable/shape_carousel_bg_default.xml new file mode 100644 index 0000000..474f14d --- /dev/null +++ b/app/src/main/res/drawable/shape_carousel_bg_default.xml @@ -0,0 +1,10 @@ + + + + + + + + + + From a45b3ffb7d1d03f1550042c2b0234b17ac835794 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:39:38 +0530 Subject: [PATCH 089/394] Change shape to ripple for cycle order badge - Refactored badge attributes for theme support --- .../main/res/drawable/shape_cycle_order_badge.xml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/drawable/shape_cycle_order_badge.xml b/app/src/main/res/drawable/shape_cycle_order_badge.xml index e8ebe00..8feb24a 100644 --- a/app/src/main/res/drawable/shape_cycle_order_badge.xml +++ b/app/src/main/res/drawable/shape_cycle_order_badge.xml @@ -1,6 +1,9 @@ - - - - + + + + + + + + From d8d0e349238f03bd4bee543e690154f6d16ad7cf Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:42:44 +0530 Subject: [PATCH 090/394] Add shape_cycle_segmented_container_rect drawable - Created container shape for cycle tabs --- .../res/drawable/shape_cycle_segmented_container_rect.xml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 app/src/main/res/drawable/shape_cycle_segmented_container_rect.xml diff --git a/app/src/main/res/drawable/shape_cycle_segmented_container_rect.xml b/app/src/main/res/drawable/shape_cycle_segmented_container_rect.xml new file mode 100644 index 0000000..3e454a5 --- /dev/null +++ b/app/src/main/res/drawable/shape_cycle_segmented_container_rect.xml @@ -0,0 +1,6 @@ + + + + + From b9d8d71404c34d40297b4297844a2702d66e575c Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:43:45 +0530 Subject: [PATCH 091/394] Change background color of shape_dialog_bg --- app/src/main/res/drawable/shape_dialog_bg.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/drawable/shape_dialog_bg.xml b/app/src/main/res/drawable/shape_dialog_bg.xml index b9a57cd..6022dce 100644 --- a/app/src/main/res/drawable/shape_dialog_bg.xml +++ b/app/src/main/res/drawable/shape_dialog_bg.xml @@ -1,6 +1,6 @@ - + From e4f99e22c3d2ac2591fa8d7691b4916f86001351 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:45:03 +0530 Subject: [PATCH 092/394] Add shape_exec_segmented_container drawable resource - Created container shape for execution tabs --- .../main/res/drawable/shape_exec_segmented_container.xml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 app/src/main/res/drawable/shape_exec_segmented_container.xml diff --git a/app/src/main/res/drawable/shape_exec_segmented_container.xml b/app/src/main/res/drawable/shape_exec_segmented_container.xml new file mode 100644 index 0000000..a53ea06 --- /dev/null +++ b/app/src/main/res/drawable/shape_exec_segmented_container.xml @@ -0,0 +1,6 @@ + + + + + From 45753d5a58812f98586b6d01b43617a761aa800e Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:46:36 +0530 Subject: [PATCH 093/394] Add shape_fab_bg.xml for FAB background - Created circular background shape for Floating Action Button --- app/src/main/res/drawable/shape_fab_bg.xml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/src/main/res/drawable/shape_fab_bg.xml diff --git a/app/src/main/res/drawable/shape_fab_bg.xml b/app/src/main/res/drawable/shape_fab_bg.xml new file mode 100644 index 0000000..6fc5754 --- /dev/null +++ b/app/src/main/res/drawable/shape_fab_bg.xml @@ -0,0 +1,8 @@ + + + + + + + + From 83b2a8c6324db6686c039d0ae0c78b83bf85af60 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:47:25 +0530 Subject: [PATCH 094/394] Delete app/src/main/res/drawable/shape_qs_hint_bg.xml - Deleted legacy shape --- app/src/main/res/drawable/shape_qs_hint_bg.xml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 app/src/main/res/drawable/shape_qs_hint_bg.xml diff --git a/app/src/main/res/drawable/shape_qs_hint_bg.xml b/app/src/main/res/drawable/shape_qs_hint_bg.xml deleted file mode 100644 index 8f2c988..0000000 --- a/app/src/main/res/drawable/shape_qs_hint_bg.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - From ff8c393919bee55c14d90f4c0c7282be69ce4ced Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:48:34 +0530 Subject: [PATCH 095/394] Delete app/src/main/res/drawable/shape_segmented_container.xml - Deleted legacy segmented container --- app/src/main/res/drawable/shape_segmented_container.xml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 app/src/main/res/drawable/shape_segmented_container.xml diff --git a/app/src/main/res/drawable/shape_segmented_container.xml b/app/src/main/res/drawable/shape_segmented_container.xml deleted file mode 100644 index 3537330..0000000 --- a/app/src/main/res/drawable/shape_segmented_container.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - From 74b43eef69418608f19af727c3b3dd0320c420fb Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:49:19 +0530 Subject: [PATCH 096/394] Delete app/src/main/res/drawable/shape_segmented_container_rect.xml - Deleted legacy segmented container --- .../main/res/drawable/shape_segmented_container_rect.xml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 app/src/main/res/drawable/shape_segmented_container_rect.xml diff --git a/app/src/main/res/drawable/shape_segmented_container_rect.xml b/app/src/main/res/drawable/shape_segmented_container_rect.xml deleted file mode 100644 index 9047d42..0000000 --- a/app/src/main/res/drawable/shape_segmented_container_rect.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - From 69f1834a9d2fdd405031f255c88fb1088feb4c28 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:50:52 +0530 Subject: [PATCH 097/394] Add shape_sim_segmented_container drawable resource - Created container shape for target SIM tabs --- app/src/main/res/drawable/shape_sim_segmented_container.xml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 app/src/main/res/drawable/shape_sim_segmented_container.xml diff --git a/app/src/main/res/drawable/shape_sim_segmented_container.xml b/app/src/main/res/drawable/shape_sim_segmented_container.xml new file mode 100644 index 0000000..b63364f --- /dev/null +++ b/app/src/main/res/drawable/shape_sim_segmented_container.xml @@ -0,0 +1,6 @@ + + + + + From 36fea89b9fb9b11b0b7d11132d213467069ff70a Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:51:23 +0530 Subject: [PATCH 098/394] Delete app/src/main/res/drawable/shape_small_divider.xml - Deleted unused divider shape --- app/src/main/res/drawable/shape_small_divider.xml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 app/src/main/res/drawable/shape_small_divider.xml diff --git a/app/src/main/res/drawable/shape_small_divider.xml b/app/src/main/res/drawable/shape_small_divider.xml deleted file mode 100644 index 9ee6d7c..0000000 --- a/app/src/main/res/drawable/shape_small_divider.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - From 8f6cf9ae58e485e81b7549157d641fb93ba7d1f1 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:52:26 +0530 Subject: [PATCH 099/394] Change shape to ripple effect for error badge - Refactored shape attributes to use semantic error colors --- .../main/res/drawable/shape_status_badge_error.xml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/drawable/shape_status_badge_error.xml b/app/src/main/res/drawable/shape_status_badge_error.xml index 697a233..f2cb057 100644 --- a/app/src/main/res/drawable/shape_status_badge_error.xml +++ b/app/src/main/res/drawable/shape_status_badge_error.xml @@ -1,6 +1,9 @@ - - - - + + + + + + + + From 9788dee95f0072453015010f56e297569ba3a780 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:53:22 +0530 Subject: [PATCH 100/394] Change shape to ripple effect for success badge - Refactored shape attributes to use semantic success colors --- .../res/drawable/shape_status_badge_success.xml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/drawable/shape_status_badge_success.xml b/app/src/main/res/drawable/shape_status_badge_success.xml index 5dc23a2..00b2e2f 100644 --- a/app/src/main/res/drawable/shape_status_badge_success.xml +++ b/app/src/main/res/drawable/shape_status_badge_success.xml @@ -1,6 +1,9 @@ - - - - + + + + + + + + From afcf10d7e5cc53119b260d99c0b54183a8f6553f Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:54:19 +0530 Subject: [PATCH 101/394] Add warning status badge drawable resource - Created shape for neutral/warning status indicators --- app/src/main/res/drawable/shape_status_badge_warning.xml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/src/main/res/drawable/shape_status_badge_warning.xml diff --git a/app/src/main/res/drawable/shape_status_badge_warning.xml b/app/src/main/res/drawable/shape_status_badge_warning.xml new file mode 100644 index 0000000..79e5bee --- /dev/null +++ b/app/src/main/res/drawable/shape_status_badge_warning.xml @@ -0,0 +1,9 @@ + + + + + + + + + From a75397edb9964d9b9fcec45cbb2468e0508e0dc4 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:55:57 +0530 Subject: [PATCH 102/394] Added target SIM card background - Created background shape for modular target SIM card --- .../main/res/drawable/shape_target_sim_card_bg.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/src/main/res/drawable/shape_target_sim_card_bg.xml diff --git a/app/src/main/res/drawable/shape_target_sim_card_bg.xml b/app/src/main/res/drawable/shape_target_sim_card_bg.xml new file mode 100644 index 0000000..130b028 --- /dev/null +++ b/app/src/main/res/drawable/shape_target_sim_card_bg.xml @@ -0,0 +1,11 @@ + + + + + + From dd3b6ab09ddef47d0881096c8cd6969e7fb998e1 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:56:51 +0530 Subject: [PATCH 103/394] Change shape drawable to ripple drawable - Refactored shape to support theming --- app/src/main/res/drawable/social_icon_bg.xml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/drawable/social_icon_bg.xml b/app/src/main/res/drawable/social_icon_bg.xml index fd5528c..24e0230 100644 --- a/app/src/main/res/drawable/social_icon_bg.xml +++ b/app/src/main/res/drawable/social_icon_bg.xml @@ -1,6 +1,10 @@ - - - - + + + + + + + + From 3be4f5a91e79b7008b8b9bd64d49cfdb07d24a5e Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:58:05 +0530 Subject: [PATCH 104/394] Add monochrome drawable to adaptive icon - Added support for monochrome themed icons in Android 13+ - Increased stroke thickness of the central antenna --- app/src/main/res/drawable-anydpi-v26/ic_launcher.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml b/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml index 6b78462..8fde456 100644 --- a/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + From b1a0f934fb88d895b758b64aa08095eb21c40763 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 3 Sep 2026 00:01:44 +0530 Subject: [PATCH 105/394] Refactor activity_main.xml layout elements - Added Theme Toggle button - Added Routine Shortcuts FAB - Replaced TargetSim radio group with view_target_sim_card include --- app/src/main/res/layout/activity_main.xml | 104 +++++++++++++++++----- 1 file changed, 80 insertions(+), 24 deletions(-) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 67f0e0c..fe95f4d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,6 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" + android:id="@+id/mainRoot" android:background="@color/surface_background"> @@ -14,8 +15,8 @@ android:paddingHorizontal="16dp" android:paddingTop="12dp" android:paddingBottom="12dp" - android:elevation="6dp" - android:outlineProvider="bounds" + android:elevation="8dp" + android:gravity="center_vertical"> @@ -44,7 +45,7 @@ android:layout_height="wrap_content" android:layout_toEndOf="@id/appIconFrame" android:layout_centerVertical="true" - android:text="NetToggle" + android:text="@string/nettoggle" android:textSize="26sp" android:fontFamily="sans-serif-black" android:textColor="@color/text_primary" @@ -55,28 +56,52 @@ android:id="@+id/appVersionText" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentEnd="true" + android:layout_toEndOf="@id/appTitleText" android:layout_centerVertical="true" + android:layout_marginStart="10dp" android:background="@drawable/shape_pill_badge_bg" android:fontFamily="sans-serif-medium" android:gravity="center" android:minHeight="24dp" android:paddingHorizontal="10dp" android:paddingVertical="3dp" - android:text="v0.0.0" + android:text="@string/v0_0_0" android:textColor="@color/brand_on_primary_container" android:textSize="13sp" android:textStyle="bold" /> + + - + + + + @@ -108,12 +133,13 @@ android:layout_marginTop="0dp" android:layout_marginBottom="12dp"> - @@ -133,7 +159,7 @@ android:textColor="@color/text_primary" android:textSize="16sp" android:textStyle="bold" - android:text="New to NetToggle?" /> + android:text="@string/new_to_nettoggle" /> + android:text="@string/learn_how_to_set_up_execution_modes" /> + android:text="@string/read_guide" /> + + + - - + + - + + android:elevation="8dp"> @@ -261,7 +297,7 @@ android:id="@+id/developerTitleText" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="DEVELOPER" + android:text="@string/developer" android:textSize="10sp" android:fontFamily="sans-serif-medium" android:textStyle="bold" @@ -272,7 +308,7 @@ android:id="@+id/developerNameText" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Dhangofa" + android:text="@string/dhangofa" android:textSize="15sp" android:fontFamily="sans-serif-medium" android:textStyle="bold" @@ -287,12 +323,12 @@ @@ -300,27 +336,47 @@ + + + + + From 66271be2a00840cfbdbd0e011111a81357577879 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 3 Sep 2026 00:03:15 +0530 Subject: [PATCH 106/394] Localized permission bottom sheet - Replaced hardcoded text with @string references --- app/src/main/res/layout/bottom_sheet_permission.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/bottom_sheet_permission.xml b/app/src/main/res/layout/bottom_sheet_permission.xml index 4f473b8..b98bf84 100644 --- a/app/src/main/res/layout/bottom_sheet_permission.xml +++ b/app/src/main/res/layout/bottom_sheet_permission.xml @@ -25,7 +25,7 @@ @@ -60,7 +60,7 @@ android:id="@+id/btnGrantPermission" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Grant Permission" + android:text="@string/grant_permission" android:paddingHorizontal="24dp" android:background="@drawable/shape_button_primary" android:textColor="@color/card_surface" /> From 3cf5cce3f4d127bb51002641b8a073acfca02d75 Mon Sep 17 00:00:00 2001 From: DK <117024745+Dhangofa@users.noreply.github.com> Date: Thu, 3 Sep 2026 00:04:18 +0530 Subject: [PATCH 107/394] Localized diagnostic dialog - Replaced hardcoded text with @string references --- app/src/main/res/layout/dialog_diagnostic.xml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/dialog_diagnostic.xml b/app/src/main/res/layout/dialog_diagnostic.xml index e348fcd..0aad2bf 100644 --- a/app/src/main/res/layout/dialog_diagnostic.xml +++ b/app/src/main/res/layout/dialog_diagnostic.xml @@ -11,7 +11,7 @@ Date: Thu, 3 Sep 2026 00:05:43 +0530 Subject: [PATCH 108/394] Added layout for routine shortcuts - Created layout with a RecyclerView for managing shortcuts --- .../res/layout/dialog_routine_shortcuts.xml | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 app/src/main/res/layout/dialog_routine_shortcuts.xml diff --git a/app/src/main/res/layout/dialog_routine_shortcuts.xml b/app/src/main/res/layout/dialog_routine_shortcuts.xml new file mode 100644 index 0000000..883c310 --- /dev/null +++ b/app/src/main/res/layout/dialog_routine_shortcuts.xml @@ -0,0 +1,66 @@ + + + + + + + +