Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 73 additions & 150 deletions app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -75,6 +81,7 @@ public void onStartListening() {
@Override
public void onClick() {
super.onClick();

if (!IS_SWITCHING.compareAndSet(false, true)) {
updateTileSwitchingUI();
return;
Expand All @@ -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);
Expand All @@ -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("?"));
Expand All @@ -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);
Expand All @@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading