diff --git a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java index 6ee741a..2149cbf 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java +++ b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java @@ -6,34 +6,29 @@ import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.drawable.Icon; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; -import android.telephony.SubscriptionManager; import android.widget.Toast; -import com.dhangofa.networktoggle.command.CommandExecutor; -import com.dhangofa.networktoggle.command.CommandExecutorFactory; import com.dhangofa.networktoggle.config.AppPreferences; import com.dhangofa.networktoggle.model.CommandResult; import com.dhangofa.networktoggle.model.ExecutionMode; import com.dhangofa.networktoggle.model.NetworkMode; -import com.dhangofa.networktoggle.model.TargetSim; +import com.dhangofa.networktoggle.telephony.NetworkModeController; +import com.dhangofa.networktoggle.telephony.NetworkModeReader; +import com.dhangofa.networktoggle.telephony.SimResolver; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class NetworkTileService extends TileService { - private static final int INVALID_SLOT_INDEX = -1; - private static final int INVALID_SUB_ID = -1; - private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(); - private static final AtomicBoolean IS_SWITCHING = new AtomicBoolean(false); - private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+"); + private static final ExecutorService EXECUTOR = + Executors.newSingleThreadExecutor(); + private static final AtomicBoolean IS_SWITCHING = + new AtomicBoolean(false); private static Icon icon4g; private static Icon icon5g; @@ -42,25 +37,36 @@ public class NetworkTileService extends TileService { private static Icon iconUnknown; private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private AppPreferences appPreferences; + private NetworkModeReader networkModeReader; + private NetworkModeController networkModeController; @Override public void onCreate() { super.onCreate(); + appPreferences = new AppPreferences(this); + SimResolver simResolver = new SimResolver(appPreferences); + networkModeReader = new NetworkModeReader(appPreferences, simResolver); + networkModeController = new NetworkModeController(simResolver); } @Override public void onStartListening() { super.onStartListening(); + NetworkMode cachedMode = appPreferences.getCachedNetworkMode(); updateTileUI(cachedMode); if (appPreferences.getExecutionMode() == ExecutionMode.NONE - || cachedMode != NetworkMode.UNKNOWN) return; + || cachedMode != NetworkMode.UNKNOWN) { + return; + } EXECUTOR.execute(() -> { - NetworkMode realMode = readCurrentNetworkMode(); + NetworkMode realMode = networkModeReader.readCurrentMode(); + mainHandler.post(() -> { if (realMode != NetworkMode.UNKNOWN) { appPreferences.setCachedNetworkMode(realMode); @@ -75,6 +81,7 @@ public void onStartListening() { @Override public void onClick() { super.onClick(); + if (!IS_SWITCHING.compareAndSet(false, true)) { updateTileSwitchingUI(); return; @@ -92,16 +99,22 @@ public void onClick() { updateTileSwitchingUI(); EXECUTOR.execute(() -> { - boolean success = applyNetworkMode(nextMode, executionMode); + CommandResult result = networkModeController.apply( + nextMode, + executionMode + ); + mainHandler.post(() -> { try { - if (success) { + if (result.isSuccess()) { appPreferences.setCachedNetworkMode(nextMode); appPreferences.setAutoSimError(false); updateTileUI(nextMode); } else { updateTileUI(currentMode); - if (appPreferences.hasAutoSimError()) showAutoSimErrorToast(); + if (appPreferences.hasAutoSimError()) { + showAutoSimErrorToast(); + } } } finally { IS_SWITCHING.set(false); @@ -112,35 +125,60 @@ public void onClick() { private Icon createTextOnlyIcon(String text) { int size = 256; - Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + 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.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 fm = paint.getFontMetrics(); - float y = (size / 2f) - (fm.descent + fm.ascent) / 2f; + 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; - default: if (iconUnknown == null) iconUnknown = createTextOnlyIcon("?"); return iconUnknown; + 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; + 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("?")); @@ -150,6 +188,7 @@ private void updateTileSwitchingUI() { private void updateTileUI(NetworkMode mode) { Tile tile = getQsTile(); if (tile == null) return; + if (mode == NetworkMode.UNKNOWN) { if (appPreferences.getExecutionMode() == ExecutionMode.NONE) { tile.setState(Tile.STATE_UNAVAILABLE); @@ -164,132 +203,16 @@ private void updateTileUI(NetworkMode mode) { tile.setLabel(mode.getTileLabel()); tile.setIcon(getCachedIcon(mode.getIconText())); } - tile.updateTile(); - } - - private NetworkMode readCurrentNetworkMode() { - ExecutionMode executionMode = appPreferences.getExecutionMode(); - if (executionMode == ExecutionMode.NONE) return NetworkMode.UNKNOWN; - - TargetSim targetSim = appPreferences.getTargetSim(); - int targetSubId = INVALID_SUB_ID; - if (targetSim == TargetSim.AUTO) { - targetSubId = SubscriptionManager.getDefaultDataSubscriptionId(); - } else { - targetSubId = resolveSubIdFromDumpsys(executionMode, targetSim.getManualSlotIndex()); - } - - String command; - if (targetSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID - && targetSubId != INVALID_SUB_ID) { - 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) { - 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 = runCommandForResult(executionMode, command); - if (result.getExitCode() != 0) return NetworkMode.UNKNOWN; - return NetworkMode.fromLegacyMode(extractFirstInt(result.getStdout())); - } - - private CommandResult runCommandForResult(ExecutionMode mode, String command) { - CommandExecutor executor = CommandExecutorFactory.forMode(mode); - if (executor == null) { - return CommandResult.failed(command, "No execution mode selected."); - } - return executor.execute(command); - } - - private Integer extractFirstInt(String text) { - try { - if (text == null || text.trim().isEmpty() || text.trim().equalsIgnoreCase("null")) return null; - Matcher matcher = NUMBER_PATTERN.matcher(text); - return matcher.find() ? Integer.parseInt(matcher.group()) : null; - } catch (Exception e) { - return null; - } - } - - private boolean isValidSlotIndex(int slotIndex) { - return slotIndex == 0 || slotIndex == 1; - } - - private void setAutoSimError(boolean value) { - appPreferences.setAutoSimError(value); + tile.updateTile(); } private void showAutoSimErrorToast() { - Toast.makeText(this, - "Unable to detect active data SIM automatically. Please choose SIM from the app.", - Toast.LENGTH_LONG).show(); - } - - private int resolveTargetSlotIndex(ExecutionMode executionMode) { - TargetSim targetSim = appPreferences.getTargetSim(); - if (!targetSim.isAuto()) { - setAutoSimError(false); - return targetSim.getManualSlotIndex(); - } - return resolveAutoSlotIndex(executionMode); + Toast.makeText( + this, + "Unable to detect active data SIM automatically. " + + "Please choose SIM from the app.", + Toast.LENGTH_LONG + ).show(); } - - private int resolveAutoSlotIndex(ExecutionMode executionMode) { - int dataSubId = SubscriptionManager.getDefaultDataSubscriptionId(); - if (dataSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - setAutoSimError(true); - return INVALID_SLOT_INDEX; - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - try { - int slot = SubscriptionManager.getSlotIndex(dataSubId); - if (isValidSlotIndex(slot)) { - setAutoSimError(false); - return slot; - } - } catch (Exception ignored) { - } - } - int slot = resolveSlotIndexFromDumpsys(executionMode, dataSubId); - if (isValidSlotIndex(slot)) { - setAutoSimError(false); - return slot; - } - setAutoSimError(true); - return INVALID_SLOT_INDEX; - } - - private int resolveSlotIndexFromDumpsys(ExecutionMode executionMode, int dataSubId) { - String command = "dumpsys isub | grep -E \"\\{id=" + dataSubId - + "([^0-9]| )\" | head -n 1 | grep -o -E \"simSlotIndex=[0-9]+\" " - + "| cut -d '=' -f 2"; - CommandResult result = runCommandForResult(executionMode, command); - Integer slot = result.getExitCode() == 0 ? extractFirstInt(result.getStdout()) : null; - return slot != null && isValidSlotIndex(slot) ? slot : INVALID_SLOT_INDEX; - } - - private int resolveSubIdFromDumpsys(ExecutionMode executionMode, int slotIndex) { - String command = "dumpsys isub | grep -E \"simSlotIndex=" + slotIndex - + "([^0-9]| )\" | head -n 1 | grep -o -E \"\\{id=[0-9]+\" " - + "| cut -d '=' -f 2"; - CommandResult result = runCommandForResult(executionMode, command); - Integer subId = result.getExitCode() == 0 ? extractFirstInt(result.getStdout()) : null; - return subId != null && subId > 0 ? subId : INVALID_SUB_ID; - } - - private boolean applyNetworkMode(NetworkMode mode, ExecutionMode executionMode) { - int slotIndex = resolveTargetSlotIndex(executionMode); - if (!isValidSlotIndex(slotIndex) || mode.getBinaryMask() == null) return false; - - String command = "cmd phone set-allowed-network-types-for-users -s " - + slotIndex + " " + mode.getBinaryMask(); - CommandResult result = runCommandForResult(executionMode, command); - return result.isSuccess(); - } - } diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeController.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeController.java new file mode 100644 index 0000000..752b678 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeController.java @@ -0,0 +1,46 @@ +package com.dhangofa.networktoggle.telephony; + +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; + +public final class NetworkModeController { + private final SimResolver simResolver; + + public NetworkModeController(SimResolver simResolver) { + this.simResolver = simResolver; + } + + public 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); + } +} diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java new file mode 100644 index 0000000..68dc0f4 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java @@ -0,0 +1,67 @@ +package com.dhangofa.networktoggle.telephony; + +import com.dhangofa.networktoggle.command.CommandExecutor; +import com.dhangofa.networktoggle.command.CommandExecutorFactory; +import com.dhangofa.networktoggle.config.AppPreferences; +import com.dhangofa.networktoggle.model.CommandResult; +import com.dhangofa.networktoggle.model.ExecutionMode; +import com.dhangofa.networktoggle.model.NetworkMode; +import com.dhangofa.networktoggle.model.TargetSim; + +public final class NetworkModeReader { + private final AppPreferences appPreferences; + private final SimResolver simResolver; + + public NetworkModeReader( + AppPreferences appPreferences, + SimResolver simResolver + ) { + this.appPreferences = appPreferences; + this.simResolver = simResolver; + } + + public NetworkMode readCurrentMode() { + ExecutionMode executionMode = appPreferences.getExecutionMode(); + if (executionMode == ExecutionMode.NONE) { + return NetworkMode.UNKNOWN; + } + + TargetSim targetSim = appPreferences.getTargetSim(); + int targetSubId = simResolver.resolveTargetSubId(executionMode); + + 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) { + 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); + } +} diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/ShellValueParser.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShellValueParser.java new file mode 100644 index 0000000..078f5af --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShellValueParser.java @@ -0,0 +1,26 @@ +package com.dhangofa.networktoggle.telephony; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +final class ShellValueParser { + private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+"); + + private ShellValueParser() { + } + + static Integer extractFirstInt(String text) { + try { + if (text == null + || text.trim().isEmpty() + || text.trim().equalsIgnoreCase("null")) { + return null; + } + + Matcher matcher = NUMBER_PATTERN.matcher(text); + return matcher.find() ? Integer.parseInt(matcher.group()) : null; + } catch (Exception ignored) { + return null; + } + } +} diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java new file mode 100644 index 0000000..3f961ba --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java @@ -0,0 +1,134 @@ +package com.dhangofa.networktoggle.telephony; + +import android.os.Build; +import android.telephony.SubscriptionManager; + +import com.dhangofa.networktoggle.command.CommandExecutor; +import com.dhangofa.networktoggle.command.CommandExecutorFactory; +import com.dhangofa.networktoggle.config.AppPreferences; +import com.dhangofa.networktoggle.model.CommandResult; +import com.dhangofa.networktoggle.model.ExecutionMode; +import com.dhangofa.networktoggle.model.TargetSim; + +public final class SimResolver { + public static final int INVALID_SLOT_INDEX = -1; + public static final int INVALID_SUB_ID = -1; + + private final AppPreferences appPreferences; + + public SimResolver(AppPreferences appPreferences) { + this.appPreferences = appPreferences; + } + + public int resolveTargetSlotIndex(ExecutionMode executionMode) { + TargetSim targetSim = appPreferences.getTargetSim(); + + if (!targetSim.isAuto()) { + appPreferences.setAutoSimError(false); + return targetSim.getManualSlotIndex(); + } + + return resolveAutoSlotIndex(executionMode); + } + + public int resolveTargetSubId(ExecutionMode executionMode) { + TargetSim targetSim = appPreferences.getTargetSim(); + + if (targetSim == TargetSim.AUTO) { + int subId = SubscriptionManager.getDefaultDataSubscriptionId(); + return isValidSubId(subId) ? subId : INVALID_SUB_ID; + } + + return resolveSubIdFromDumpsys( + executionMode, + targetSim.getManualSlotIndex() + ); + } + + public boolean isValidSlotIndex(int slotIndex) { + return slotIndex == 0 || slotIndex == 1; + } + + public boolean isValidSubId(int subId) { + return subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID + && subId != INVALID_SUB_ID; + } + + private int resolveAutoSlotIndex(ExecutionMode executionMode) { + int dataSubId = SubscriptionManager.getDefaultDataSubscriptionId(); + + if (!isValidSubId(dataSubId)) { + appPreferences.setAutoSimError(true); + return INVALID_SLOT_INDEX; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + int slotIndex = SubscriptionManager.getSlotIndex(dataSubId); + if (isValidSlotIndex(slotIndex)) { + appPreferences.setAutoSimError(false); + return slotIndex; + } + } catch (Exception ignored) { + } + } + + int slotIndex = resolveSlotIndexFromDumpsys(executionMode, dataSubId); + if (isValidSlotIndex(slotIndex)) { + appPreferences.setAutoSimError(false); + return slotIndex; + } + + appPreferences.setAutoSimError(true); + return INVALID_SLOT_INDEX; + } + + private int resolveSlotIndexFromDumpsys( + ExecutionMode executionMode, + int dataSubId + ) { + String command = "dumpsys isub | grep -E \"\\{id=" + dataSubId + + "([^0-9]| )\" | head -n 1 " + + "| grep -o -E \"simSlotIndex=[0-9]+\" " + + "| cut -d '=' -f 2"; + + CommandResult result = execute(executionMode, command); + Integer slotIndex = result.isSuccess() + ? ShellValueParser.extractFirstInt(result.getStdout()) + : null; + + return slotIndex != null && isValidSlotIndex(slotIndex) + ? slotIndex + : INVALID_SLOT_INDEX; + } + + private int resolveSubIdFromDumpsys( + ExecutionMode executionMode, + int slotIndex + ) { + String command = "dumpsys isub | grep -E \"simSlotIndex=" + slotIndex + + "([^0-9]| )\" | head -n 1 " + + "| grep -o -E \"\\{id=[0-9]+\" " + + "| cut -d '=' -f 2"; + + CommandResult result = execute(executionMode, command); + Integer subId = result.isSuccess() + ? ShellValueParser.extractFirstInt(result.getStdout()) + : null; + + return subId != null && subId > 0 + ? subId + : INVALID_SUB_ID; + } + + 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); + } +}