diff --git a/app/src/main/java/com/dhangofa/networktoggle/BootReceiver.java b/app/src/main/java/com/dhangofa/networktoggle/BootReceiver.java index 5deb047..99ba90b 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/BootReceiver.java +++ b/app/src/main/java/com/dhangofa/networktoggle/BootReceiver.java @@ -3,34 +3,20 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; -public class BootReceiver extends BroadcastReceiver { - - private static final String PREFS_NAME = "NetTogglePrefs"; - private static final String STATE_KEY = "net_state"; - private static final String AUTO_SIM_ERROR_KEY = "auto_sim_error"; - - private static final int STATE_UNKNOWN = 0; +import com.dhangofa.networktoggle.config.AppPreferences; +public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - if (context == null || intent == null) { - return; - } + if (context == null || intent == null) return; String action = intent.getAction(); - if (!Intent.ACTION_BOOT_COMPLETED.equals(action) && !Intent.ACTION_MY_PACKAGE_REPLACED.equals(action)) { return; } - SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - - prefs.edit() - .putInt(STATE_KEY, STATE_UNKNOWN) - .putBoolean(AUTO_SIM_ERROR_KEY, false) - .apply(); + new AppPreferences(context).clearTransientState(); } } diff --git a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java index b07b69f..b66c6f6 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java +++ b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java @@ -21,381 +21,305 @@ import android.app.Activity; import android.content.Intent; -import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.Typeface; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.view.Gravity; +import android.view.View; import android.widget.ImageView; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.TextView; -import android.view.View; + +import com.dhangofa.networktoggle.config.AppPreferences; +import com.dhangofa.networktoggle.model.ExecutionMode; +import com.dhangofa.networktoggle.model.TargetSim; import rikka.shizuku.Shizuku; public class MainActivity extends Activity { - - private static final String PREFS_NAME = "NetTogglePrefs"; - private static final String EXEC_MODE_KEY = "exec_mode"; - private static final String TARGET_SIM_KEY = "target_sim"; - private static final String AUTO_SIM_ERROR_KEY = "auto_sim_error"; - private static final String STATE_KEY = "net_state"; - - private static final int MODE_NONE = 0; - private static final int MODE_ROOT = 1; - private static final int MODE_SHIZUKU = 2; - private static final int STATE_UNKNOWN = 0; - - private static final int TARGET_SIM_AUTO = 0; - private static final int TARGET_SIM_1 = 1; - private static final int TARGET_SIM_2 = 2; - private RadioGroup radioGroup; private RadioButton radioRoot; private RadioButton radioShizuku; private TextView statusText; - private ImageView githubLink; - private ImageView telegramLink; - private SharedPreferences prefs; - - private RadioGroup targetSimRadioGroup; - private RadioButton radioSimAuto; - private RadioButton radioSim1; - private RadioButton radioSim2; - private TextView autoSimWarningText; - - private volatile boolean activityDestroyed = false; - private Thread rootCheckThread; - private Process rootCheckProcess; - - // Wait for Shizuku Binder, then check permission only if Shizuku mode is selected. - private final Shizuku.OnBinderReceivedListener binderReceivedListener = () -> { - runOnUiThread(() -> { - if (!activityDestroyed - && prefs != null - && prefs.getInt(EXEC_MODE_KEY, MODE_NONE) == MODE_SHIZUKU) { - checkShizukuPermission(false); - } - }); - }; - - // Handle Shizuku service death only when Shizuku mode is selected. - private final Shizuku.OnBinderDeadListener binderDeadListener = () -> { - runOnUiThread(() -> { - if (!activityDestroyed - && prefs != null - && prefs.getInt(EXEC_MODE_KEY, MODE_NONE) == MODE_SHIZUKU) { - statusText.setText("Shizuku is not running."); - statusText.setTextColor(0xFFFF5555); - } - }); - }; - - - // React when the user grants or denies Shizuku permission. - private final Shizuku.OnRequestPermissionResultListener permissionResultListener = (requestCode, grantResult) -> { - runOnUiThread(() -> { - if (!activityDestroyed - && prefs != null - && prefs.getInt(EXEC_MODE_KEY, MODE_NONE) == MODE_SHIZUKU) { - checkShizukuPermission(false); - } - }); - }; + private ImageView githubLink; + private ImageView telegramLink; + + private RadioGroup targetSimRadioGroup; + private RadioButton radioSimAuto; + private RadioButton radioSim1; + private RadioButton radioSim2; + private TextView autoSimWarningText; + + private AppPreferences appPreferences; + 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); + } + }); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); activityDestroyed = false; - - // Inject custom version pill into the default Action Bar - if (getActionBar() != null) { - getActionBar().setDisplayOptions( - android.app.ActionBar.DISPLAY_SHOW_TITLE | android.app.ActionBar.DISPLAY_SHOW_CUSTOM); - - android.widget.TextView versionText = new android.widget.TextView(this); - versionText.setText("v" + getAppVersionName()); - versionText.setTextSize(12); - versionText.setTypeface(null, android.graphics.Typeface.BOLD); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - versionText.setTextColor(getColor(R.color.brand_on_primary_container)); - } - versionText.setBackgroundResource(R.drawable.shape_pill_badge_bg); - int padX = (int) (10 * getResources().getDisplayMetrics().density); - int padY = (int) (4 * getResources().getDisplayMetrics().density); - versionText.setPadding(padX, padY, padX, padY); - - android.app.ActionBar.LayoutParams layoutParams = new android.app.ActionBar.LayoutParams( - android.app.ActionBar.LayoutParams.WRAP_CONTENT, - android.app.ActionBar.LayoutParams.WRAP_CONTENT, - android.view.Gravity.END | android.view.Gravity.CENTER_VERTICAL); - layoutParams.setMarginEnd((int) (16 * getResources().getDisplayMetrics().density)); - - getActionBar().setCustomView(versionText, layoutParams); - getActionBar().setElevation(0); - // Try to make the default Action Bar title bold - try { - int titleId = getResources().getIdentifier("action_bar_title", "id", "android"); - android.widget.TextView titleText = findViewById(titleId); - if (titleText != null) { - titleText.setTypeface(null, android.graphics.Typeface.BOLD); - } - } catch (Exception ignored) {} - } - - // Keep status bar icon contrast readable in light and dark themes - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - boolean isNight = (getResources().getConfiguration().uiMode & android.content.res.Configuration.UI_MODE_NIGHT_MASK) == android.content.res.Configuration.UI_MODE_NIGHT_YES; - getWindow().setStatusBarColor(getColor(R.color.surface_background)); - View decor = getWindow().getDecorView(); - if (!isNight) { - decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); - } else { - decor.setSystemUiVisibility(0); - } + + configureActionBar(); + configureStatusBar(); + setContentView(R.layout.activity_main); + + appPreferences = new AppPreferences(this); + bindViews(); + bindLinks(); + registerShizukuListeners(); + loadSavedExecutionMode(); + loadSavedTargetSimMode(); + updateAutoSimWarning(); + bindSelectionListeners(); + } + + private void configureActionBar() { + if (getActionBar() == null) return; + + getActionBar().setDisplayOptions( + android.app.ActionBar.DISPLAY_SHOW_TITLE + | android.app.ActionBar.DISPLAY_SHOW_CUSTOM); + + TextView versionText = new TextView(this); + versionText.setText("v" + getAppVersionName()); + versionText.setTextSize(12); + versionText.setTypeface(null, Typeface.BOLD); + versionText.setTextColor(getColor(R.color.brand_on_primary_container)); + versionText.setBackgroundResource(R.drawable.shape_pill_badge_bg); + + float density = getResources().getDisplayMetrics().density; + versionText.setPadding((int) (10 * density), (int) (4 * density), + (int) (10 * density), (int) (4 * density)); + + android.app.ActionBar.LayoutParams params = new android.app.ActionBar.LayoutParams( + android.app.ActionBar.LayoutParams.WRAP_CONTENT, + android.app.ActionBar.LayoutParams.WRAP_CONTENT, + Gravity.END | Gravity.CENTER_VERTICAL); + params.setMarginEnd((int) (16 * density)); + + getActionBar().setCustomView(versionText, params); + getActionBar().setElevation(0); + + try { + int titleId = getResources().getIdentifier("action_bar_title", "id", "android"); + TextView titleText = findViewById(titleId); + if (titleText != null) titleText.setTypeface(null, Typeface.BOLD); + } catch (Exception ignored) { } + } - setContentView(R.layout.activity_main); + private void configureStatusBar() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return; + + boolean isNight = (getResources().getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + getWindow().setStatusBarColor(getColor(R.color.surface_background)); + getWindow().getDecorView().setSystemUiVisibility( + isNight ? 0 : View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } - prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - + private void bindViews() { radioGroup = findViewById(R.id.modeRadioGroup); radioRoot = findViewById(R.id.radioRoot); radioShizuku = findViewById(R.id.radioShizuku); statusText = findViewById(R.id.shizukuStatusText); - - 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); - - githubLink = findViewById(R.id.githubLink); - telegramLink = findViewById(R.id.telegramLink); - - githubLink.setOnClickListener(v -> openUrl("https://github.com/Dhangofa/NetToggle")); - telegramLink.setOnClickListener(v -> openUrl("https://t.me/dhangofa")); - - // Register the lifecycle listeners + 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); + githubLink = findViewById(R.id.githubLink); + telegramLink = findViewById(R.id.telegramLink); + } + + private void bindLinks() { + githubLink.setOnClickListener(v -> openUrl("https://github.com/Dhangofa/NetToggle")); + telegramLink.setOnClickListener(v -> openUrl("https://t.me/dhangofa")); + } + + private void registerShizukuListeners() { Shizuku.addBinderReceivedListener(binderReceivedListener); Shizuku.addBinderDeadListener(binderDeadListener); Shizuku.addRequestPermissionResultListener(permissionResultListener); + } - // Load saved mode (Default =0, Root = 1, Shizuku = 2) - int savedMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); - if (savedMode == MODE_ROOT) { + private void loadSavedExecutionMode() { + ExecutionMode savedMode = appPreferences.getExecutionMode(); + if (savedMode == ExecutionMode.ROOT) { radioRoot.setChecked(true); checkRootPermission(); - } else if (savedMode == MODE_SHIZUKU) { + } else if (savedMode == ExecutionMode.SHIZUKU) { radioShizuku.setChecked(true); checkShizukuPermission(false); } else { radioGroup.clearCheck(); - statusText.setText("Select Root or Shizuku mode."); - statusText.setTextColor(0xFFFFB300); + setStatus("Select Root or Shizuku mode.", 0xFFFFB300); } - - loadSavedTargetSimMode(); - updateAutoSimWarning(); - - radioGroup.setOnCheckedChangeListener((group, checkedId) -> { - if (checkedId == R.id.radioRoot) { - prefs.edit() - .putInt(EXEC_MODE_KEY, MODE_ROOT) - .putInt(STATE_KEY, STATE_UNKNOWN) - .putBoolean(AUTO_SIM_ERROR_KEY, false) - .apply(); - - checkRootPermission(); - } else if (checkedId == R.id.radioShizuku) { - prefs.edit() - .putInt(EXEC_MODE_KEY, MODE_SHIZUKU) - .putInt(STATE_KEY, STATE_UNKNOWN) - .putBoolean(AUTO_SIM_ERROR_KEY, false) - .apply(); - - checkShizukuPermission(true); - } - }); - - targetSimRadioGroup.setOnCheckedChangeListener((group, checkedId) -> { - SharedPreferences.Editor editor = prefs.edit(); - - if (checkedId == R.id.radioSimAuto) { - editor.putInt(TARGET_SIM_KEY, TARGET_SIM_AUTO); - } else if (checkedId == R.id.radioSim1) { - editor.putInt(TARGET_SIM_KEY, TARGET_SIM_1); - } else if (checkedId == R.id.radioSim2) { - editor.putInt(TARGET_SIM_KEY, TARGET_SIM_2); - } - - editor.putInt(STATE_KEY, STATE_UNKNOWN); - editor.putBoolean(AUTO_SIM_ERROR_KEY, false); - editor.apply(); - - updateAutoSimWarning(); - }); } - @Override - protected void onResume() { - super.onResume(); - - if (prefs != null) { - updateAutoSimWarning(); - } - } - - @Override - protected void onDestroy() { - activityDestroyed = true; - - // Prevent memory leaks by destroying Shizuku listeners when the app closes - Shizuku.removeBinderReceivedListener(binderReceivedListener); - Shizuku.removeBinderDeadListener(binderDeadListener); - Shizuku.removeRequestPermissionResultListener(permissionResultListener); - - // Stop any running root permission check process - if (rootCheckProcess != null) { - rootCheckProcess.destroy(); - rootCheckProcess = null; - } - - // Interrupt root check thread if it is still active - if (rootCheckThread != null && rootCheckThread.isAlive()) { - rootCheckThread.interrupt(); - rootCheckThread = null; - } - - super.onDestroy(); - } + private void bindSelectionListeners() { + radioGroup.setOnCheckedChangeListener((group, checkedId) -> { + if (checkedId == R.id.radioRoot) { + appPreferences.onExecutionModeChanged(ExecutionMode.ROOT); + checkRootPermission(); + } else if (checkedId == R.id.radioShizuku) { + appPreferences.onExecutionModeChanged(ExecutionMode.SHIZUKU); + checkShizukuPermission(true); + } + }); + + targetSimRadioGroup.setOnCheckedChangeListener((group, checkedId) -> { + TargetSim target = TargetSim.AUTO; + if (checkedId == R.id.radioSim1) target = TargetSim.SIM_1; + else if (checkedId == R.id.radioSim2) target = TargetSim.SIM_2; + appPreferences.onTargetSimChanged(target); + updateAutoSimWarning(); + }); + } + + @Override + protected void onResume() { + super.onResume(); + if (appPreferences != null) updateAutoSimWarning(); + } + + @Override + protected void onDestroy() { + activityDestroyed = true; + Shizuku.removeBinderReceivedListener(binderReceivedListener); + Shizuku.removeBinderDeadListener(binderDeadListener); + Shizuku.removeRequestPermissionResultListener(permissionResultListener); + + if (rootCheckProcess != null) { + rootCheckProcess.destroy(); + rootCheckProcess = null; + } + if (rootCheckThread != null && rootCheckThread.isAlive()) { + rootCheckThread.interrupt(); + rootCheckThread = null; + } + super.onDestroy(); + } private void checkRootPermission() { - statusText.setText("Checking root permission..."); - statusText.setTextColor(0xFFFFB300); - - rootCheckThread = new Thread(() -> { - boolean granted = false; - Process process = null; - - try { - process = Runtime.getRuntime().exec(new String[]{"su", "-c", "id"}); - rootCheckProcess = process; - - int exitCode = process.waitFor(); - granted = exitCode == 0; - } catch (Exception ignored) { - granted = false; - } finally { - if (process != null) { - process.destroy(); - } - - if (rootCheckProcess == process) { - rootCheckProcess = null; - } - } - - boolean finalGranted = granted; - - runOnUiThread(() -> { - if (activityDestroyed) { - return; - } - - if (prefs == null || prefs.getInt(EXEC_MODE_KEY, MODE_NONE) != MODE_ROOT) { - return; - } - - if (finalGranted) { - statusText.setText("Root mode active & authorized!"); - statusText.setTextColor(0xFF1B873F); - } else { - statusText.setText("Root permission denied or unavailable."); - statusText.setTextColor(0xFFFF5555); - } - }); - }); - - rootCheckThread.start(); - } - + 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); + else setStatus("Root permission denied or unavailable.", 0xFFFF5555); + }); + }); + rootCheckThread.start(); + } + private void checkShizukuPermission(boolean requestIfNeeded) { - if (activityDestroyed) { - return; - } - try { - if (!Shizuku.pingBinder()) { - statusText.setText("Shizuku is not running."); - statusText.setTextColor(0xFFFF5555); - return; - } - - if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { - statusText.setText("Shizuku mode active & authorized!"); - statusText.setTextColor(0xFF1B873F); - return; - } - - statusText.setText("Shizuku permission not granted."); - statusText.setTextColor(0xFFFFB300); - - if (requestIfNeeded) { - statusText.setText("Requesting Shizuku permission..."); - statusText.setTextColor(0xFFFFB300); - Shizuku.requestPermission(0); - } - } catch (Exception e) { - statusText.setText("Shizuku check failed."); - statusText.setTextColor(0xFFFF5555); - } - } - - private String getAppVersionName() { - try { - PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0); - return packageInfo.versionName; - } catch (Exception e) { - return "unknown"; - } - } - - private void openUrl(String url) { - try { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivity(intent); - } catch (Exception ignored) { - } - } - - private void loadSavedTargetSimMode() { - int targetSim = prefs.getInt(TARGET_SIM_KEY, TARGET_SIM_AUTO); - - if (targetSim == TARGET_SIM_1) { - radioSim1.setChecked(true); - } else if (targetSim == TARGET_SIM_2) { - radioSim2.setChecked(true); - } else { - radioSimAuto.setChecked(true); - } - } - - private void updateAutoSimWarning() { - if (autoSimWarningText == null || prefs == null) { - return; - } - - boolean hasAutoSimError = prefs.getBoolean(AUTO_SIM_ERROR_KEY, false); - int targetSim = prefs.getInt(TARGET_SIM_KEY, TARGET_SIM_AUTO); - - if (hasAutoSimError && targetSim == TARGET_SIM_AUTO) { - autoSimWarningText.setVisibility(View.VISIBLE); - autoSimWarningText.setText("Auto SIM detection failed. Please choose SIM 1 or SIM 2 manually."); - autoSimWarningText.setTextColor(0xFFFF5555); - } else { - autoSimWarningText.setVisibility(View.GONE); - } - } - + if (activityDestroyed) return; + try { + if (!Shizuku.pingBinder()) { + setStatus("Shizuku is not running.", 0xFFFF5555); + return; + } + if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { + setStatus("Shizuku mode active & authorized!", 0xFF1B873F); + return; + } + setStatus("Shizuku permission not granted.", 0xFFFFB300); + if (requestIfNeeded) { + setStatus("Requesting Shizuku permission...", 0xFFFFB300); + Shizuku.requestPermission(0); + } + } catch (Exception e) { + setStatus("Shizuku check failed.", 0xFFFF5555); + } + } + + private void setStatus(String text, int color) { + statusText.setText(text); + statusText.setTextColor(color); + } + + private String getAppVersionName() { + try { + PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), 0); + return info.versionName; + } catch (Exception e) { + return "unknown"; + } + } + + private void openUrl(String url) { + try { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(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); + } + } } + diff --git a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java index ccf7d68..72e1e50 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java +++ b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java @@ -1,736 +1,384 @@ package com.dhangofa.networktoggle; -import android.content.SharedPreferences; +import android.content.pm.PackageManager; 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; -import android.service.quicksettings.Tile; -import android.service.quicksettings.TileService; +import android.os.Build; import android.os.Handler; import android.os.Looper; -import android.os.Build; +import android.service.quicksettings.Tile; +import android.service.quicksettings.TileService; import android.telephony.SubscriptionManager; import android.widget.Toast; -import java.util.concurrent.atomic.AtomicBoolean; -import java.lang.reflect.Method; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; + +import com.dhangofa.networktoggle.config.AppPreferences; +import com.dhangofa.networktoggle.model.ExecutionMode; +import com.dhangofa.networktoggle.model.NetworkMode; +import com.dhangofa.networktoggle.model.TargetSim; + import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import java.util.regex.Pattern; + import rikka.shizuku.Shizuku; public class NetworkTileService extends TileService { + private static final int INVALID_SLOT_INDEX = -1; + private static final int INVALID_SUB_ID = -1; + private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(); + private static final AtomicBoolean IS_SWITCHING = new AtomicBoolean(false); + private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+"); + private static Method shizukuNewProcessMethod; + + private static Icon icon4g; + private static Icon icon5g; + private static Icon iconP5g; + private static Icon iconP4g; + private static Icon iconUnknown; + + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private AppPreferences appPreferences; + + @Override + public void onCreate() { + super.onCreate(); + appPreferences = new AppPreferences(this); + } + + @Override + public void onStartListening() { + super.onStartListening(); + NetworkMode cachedMode = appPreferences.getCachedNetworkMode(); + updateTileUI(cachedMode); + + if (appPreferences.getExecutionMode() == ExecutionMode.NONE + || cachedMode != NetworkMode.UNKNOWN) return; + + EXECUTOR.execute(() -> { + NetworkMode realMode = readCurrentNetworkMode(); + mainHandler.post(() -> { + if (realMode != NetworkMode.UNKNOWN) { + appPreferences.setCachedNetworkMode(realMode); + updateTileUI(realMode); + } else { + updateTileUI(cachedMode); + } + }); + }); + } + + @Override + public void onClick() { + super.onClick(); + if (!IS_SWITCHING.compareAndSet(false, true)) { + updateTileSwitchingUI(); + return; + } + + ExecutionMode executionMode = appPreferences.getExecutionMode(); + if (executionMode == ExecutionMode.NONE) { + IS_SWITCHING.set(false); + updateTileUI(NetworkMode.UNKNOWN); + return; + } + + NetworkMode currentMode = appPreferences.getCachedNetworkMode(); + NetworkMode nextMode = NetworkMode.nextInDefaultCycle(currentMode); + updateTileSwitchingUI(); + + EXECUTOR.execute(() -> { + boolean success = applyNetworkMode(nextMode, executionMode); + mainHandler.post(() -> { + try { + if (success) { + appPreferences.setCachedNetworkMode(nextMode); + appPreferences.setAutoSimError(false); + updateTileUI(nextMode); + } else { + updateTileUI(currentMode); + if (appPreferences.hasAutoSimError()) showAutoSimErrorToast(); + } + } finally { + IS_SWITCHING.set(false); + } + }); + }); + } + + private static Method getShizukuNewProcessMethod() throws NoSuchMethodException { + if (shizukuNewProcessMethod == null) { + shizukuNewProcessMethod = Shizuku.class.getDeclaredMethod( + "newProcess", String[].class, String[].class, String.class); + shizukuNewProcessMethod.setAccessible(true); + } + return shizukuNewProcessMethod; + } + + private static class CommandResult { + final int exitCode; + final String stdout; + CommandResult(int exitCode, String stdout) { + this.exitCode = exitCode; + this.stdout = stdout; + } + } - private static final String PREFS_NAME = "NetTogglePrefs"; - private static final String STATE_KEY = "net_state"; - private static final String EXEC_MODE_KEY = "exec_mode"; - - private static final int MODE_NONE = 0; - private static final int MODE_ROOT = 1; - private static final int MODE_SHIZUKU = 2; - - private static final int STATE_UNKNOWN = 0; - private static final int STATE_4G_ONLY = 1; - private static final int STATE_5G_ONLY = 2; - private static final int STATE_PREF_5G = 3; - private static final int STATE_PREF_4G = 4; - - private static final String BIN_4G_ONLY = "1000000000000"; // Legacy Id 11, bitmask 4096 - private static final String BIN_5G_ONLY = "10000000000000000000"; // Legacy Id 23, bitmask 524288 - private static final String BIN_PREF_5G = "11011111101111111111"; // Legacy Id 33, bitmask 916479 - private static final String BIN_PREF_4G = "1001101001110000111"; // Legacy Id 9, bitmask 316295 - - private static final int LEGACY_4G_ONLY = 11; - private static final int LEGACY_5G_ONLY = 23; - private static final int LEGACY_PREF_5G = 33; - private static final int LEGACY_PREF_4G = 9; - - private static final String TARGET_SIM_KEY = "target_sim"; - private static final String AUTO_SIM_ERROR_KEY = "auto_sim_error"; - - private static final int TARGET_SIM_AUTO = 0; - private static final int TARGET_SIM_1 = 1; - private static final int TARGET_SIM_2 = 2; - - 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 final Handler mainHandler = new Handler(Looper.getMainLooper()); - - private static Method SHIZUKU_NEW_PROCESS_METHOD; - - private static Icon ICON_4G; - private static Icon ICON_5G; - private static Icon ICON_P5G; - private static Icon ICON_P4G; - private static Icon ICON_UNKNOWN; - - @Override - public void onStartListening() { - super.onStartListening(); - - SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - int cachedState = prefs.getInt(STATE_KEY, STATE_UNKNOWN); - - updateTileUI(cachedState); - - int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); - if (execMode == MODE_NONE) { - return; - } - - // Lightweight behavior: - // Only read real system mode if we do not have a cached state yet. - if (cachedState != STATE_UNKNOWN) { - return; - } - - EXECUTOR.execute(() -> { - int realState = readCurrentNetworkState(); - - mainHandler.post(() -> { - if (realState != STATE_UNKNOWN) { - prefs.edit().putInt(STATE_KEY, realState).apply(); - updateTileUI(realState); - } else { - updateTileUI(cachedState); - } - }); - }); - } - - @Override - public void onClick() { - super.onClick(); - - if (!IS_SWITCHING.compareAndSet(false, true)) { - updateTileSwitchingUI(); - return; - } - - SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - - int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); - if (execMode == MODE_NONE) { - IS_SWITCHING.set(false); - updateTileUI(STATE_UNKNOWN); - return; - } - - int currentState = prefs.getInt(STATE_KEY, STATE_UNKNOWN); - - int nextState = getNextState(currentState); - String targetBinary = getBinaryForState(nextState); - - updateTileSwitchingUI(); - - EXECUTOR.execute(() -> { - boolean success = applyNetworkMode(targetBinary); - - mainHandler.post(() -> { - try { - if (success) { - prefs.edit() - .putInt(STATE_KEY, nextState) - .putBoolean(AUTO_SIM_ERROR_KEY, false) - .apply(); - - updateTileUI(nextState); - } else { - updateTileUI(currentState); - - if (prefs.getBoolean(AUTO_SIM_ERROR_KEY, false)) { - showAutoSimErrorToast(); - } - } - } finally { - IS_SWITCHING.set(false); - } - }); - }); - } - - private int getNextState(int currentState) { - switch (currentState) { - case STATE_4G_ONLY: - return STATE_5G_ONLY; - - case STATE_5G_ONLY: - return STATE_PREF_5G; - - case STATE_PREF_5G: - return STATE_PREF_4G; - - case STATE_PREF_4G: - case STATE_UNKNOWN: - default: - return STATE_4G_ONLY; - } - } - - private String getBinaryForState(int state) { - switch (state) { - case STATE_5G_ONLY: - return BIN_5G_ONLY; - - case STATE_PREF_5G: - return BIN_PREF_5G; - - case STATE_PREF_4G: - return BIN_PREF_4G; - - case STATE_4G_ONLY: - default: - return BIN_4G_ONLY; - } - } - - private static Method getShizukuNewProcessMethod() throws NoSuchMethodException { - if (SHIZUKU_NEW_PROCESS_METHOD == null) { - SHIZUKU_NEW_PROCESS_METHOD = Shizuku.class.getDeclaredMethod( - "newProcess", - String[].class, - String[].class, - String.class - ); - SHIZUKU_NEW_PROCESS_METHOD.setAccessible(true); - } - - return SHIZUKU_NEW_PROCESS_METHOD; - } - - private static class CommandResult { - final int exitCode; - final String stdout; - - CommandResult(int exitCode, String stdout) { - this.exitCode = exitCode; - this.stdout = stdout; - } - } - private Icon createTextOnlyIcon(String text) { - int size = 256; + 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 textWidth = paint.measureText(text); - if (textWidth > 240f) { - paint.setTextScaleX(240f / textWidth); - } - + 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; canvas.drawText(text, size / 2f, y, paint); - return Icon.createWithBitmap(bitmap); } - - private Icon getCachedIcon(String text) { - switch (text) { - case "4G": - if (ICON_4G == null) { - ICON_4G = createTextOnlyIcon("4G"); - } - return ICON_4G; - - case "5G": - if (ICON_5G == null) { - ICON_5G = createTextOnlyIcon("5G"); - } - return ICON_5G; - - case "P5G": - if (ICON_P5G == null) { - ICON_P5G = createTextOnlyIcon("P5G"); - } - return ICON_P5G; - - case "P4G": - if (ICON_P4G == null) { - ICON_P4G = createTextOnlyIcon("P4G"); - } - return ICON_P4G; - - default: - if (ICON_UNKNOWN == null) { - ICON_UNKNOWN = createTextOnlyIcon("?"); - } - return ICON_UNKNOWN; - } - } - - private void updateTileSwitchingUI() { - Tile tile = getQsTile(); - if (tile == null) return; - - tile.setState(Tile.STATE_INACTIVE); - tile.setLabel("Switching..."); - tile.setIcon(getCachedIcon("?")); - tile.updateTile(); - } - - private void updateTileUI(int state) { - Tile tile = getQsTile(); - if (tile == null) return; - - switch (state) { - case STATE_4G_ONLY: - tile.setState(Tile.STATE_ACTIVE); - tile.setLabel("4G Only"); - tile.setIcon(getCachedIcon("4G")); - break; - - case STATE_5G_ONLY: - tile.setState(Tile.STATE_ACTIVE); - tile.setLabel("5G Only"); - tile.setIcon(getCachedIcon("5G")); - break; - - case STATE_PREF_5G: - tile.setState(Tile.STATE_ACTIVE); - tile.setLabel("Pref 5G"); - tile.setIcon(getCachedIcon("P5G")); - break; - - case STATE_PREF_4G: - tile.setState(Tile.STATE_ACTIVE); - tile.setLabel("Pref 4G"); - tile.setIcon(getCachedIcon("P4G")); - break; - - case STATE_UNKNOWN: - default: - SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); - - if (execMode == MODE_NONE) { - tile.setState(Tile.STATE_UNAVAILABLE); - tile.setLabel("Setup Required"); - } else { - tile.setState(Tile.STATE_INACTIVE); - tile.setLabel("Tap to Set 4G"); - } - - tile.setIcon(getCachedIcon("?")); - break; - } - - tile.updateTile(); - } - - private int readCurrentNetworkState() { - SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); - - if (execMode == MODE_NONE) { - return STATE_UNKNOWN; - } - - int targetSim = prefs.getInt(TARGET_SIM_KEY, TARGET_SIM_AUTO); - int targetSubId = INVALID_SUB_ID; - - if (targetSim == TARGET_SIM_AUTO) { - targetSubId = SubscriptionManager.getDefaultDataSubscriptionId(); - } else if (targetSim == TARGET_SIM_1) { - targetSubId = resolveSubIdFromDumpsys(execMode, 0); - } else if (targetSim == TARGET_SIM_2) { - targetSubId = resolveSubIdFromDumpsys(execMode, 1); - } - - 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 == TARGET_SIM_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 STATE_UNKNOWN; - } - - CommandResult result; - - if (execMode == MODE_SHIZUKU) { - result = runCommandForResultWithShizuku(command); - } else if (execMode == MODE_ROOT) { - result = runCommandForResultWithRoot(command); - } else { - return STATE_UNKNOWN; - } - - if (result.exitCode != 0) { - return STATE_UNKNOWN; - } - - return mapLegacyNetworkModeToState(result.stdout); - } - - private CommandResult runCommandForResultWithRoot(String command) { - Process process = null; - - try { - process = Runtime.getRuntime().exec(new String[]{"su", "-c", command}); - - int exitCode = process.waitFor(); - String stdout = readStream(process.getInputStream()); - - return new CommandResult(exitCode, stdout); - } catch (Exception e) { - return new CommandResult(-1, ""); - } finally { - if (process != null) { - process.destroy(); - } - } - } - - private CommandResult runCommandForResultWithShizuku(String command) { - Process process = null; - - try { - if (!Shizuku.pingBinder()) { - return new CommandResult(-1, ""); - } - - if (Shizuku.checkSelfPermission() != android.content.pm.PackageManager.PERMISSION_GRANTED) { - return new CommandResult(-1, ""); - } - - process = (Process) getShizukuNewProcessMethod().invoke( - null, - new String[]{"sh", "-c", command}, - null, - null - ); - - if (process == null) { - return new CommandResult(-1, ""); - } - - int exitCode = process.waitFor(); - String stdout = readStream(process.getInputStream()); - - return new CommandResult(exitCode, stdout); - } catch (Exception e) { - return new CommandResult(-1, ""); - } finally { - if (process != null) { - process.destroy(); - } - } - } - - private String readStream(InputStream inputStream) { - StringBuilder builder = new StringBuilder(); - - try { - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - String line; - - while ((line = reader.readLine()) != null) { - builder.append(line).append('\n'); - } - } catch (Exception ignored) { - } - - return builder.toString().trim(); - } - - private Integer extractFirstInt(String text) { - try { - if (text == null || text.trim().isEmpty() || text.trim().equalsIgnoreCase("null")) { - return null; - } - - Matcher matcher = NUMBER_PATTERN.matcher(text); - - if (matcher.find()) { - return Integer.parseInt(matcher.group()); - } - - return null; - } catch (Exception e) { - return null; - } - } - - private int mapLegacyNetworkModeToState(String output) { - Integer legacyMode = extractFirstInt(output); - - if (legacyMode == null) { - return STATE_UNKNOWN; - } - - switch (legacyMode) { - case LEGACY_4G_ONLY: - return STATE_4G_ONLY; - - case LEGACY_5G_ONLY: - return STATE_5G_ONLY; - - case LEGACY_PREF_5G: - return STATE_PREF_5G; - - case LEGACY_PREF_4G: - return STATE_PREF_4G; - - // Preferred 4G / LTE-preferred variants across OEMs/ROMs - case 8: // CDMA + LTE/EvDo (PRL) - case 10: // LTE/CDMA/EvDo/GSM/WCDMA (PRL) - case 12: - case 15: - case 17: - case 19: - case 20: - case 22: - return STATE_PREF_4G; - - default: - // Preferred 5G / NR-capable preferred variants across OEMs/ROMs - if (legacyMode >= 24 && legacyMode <= 32) { - return STATE_PREF_5G; - } - - return STATE_UNKNOWN; - } - } - - private boolean isValidSlotIndex(int slotIndex) { - return slotIndex == 0 || slotIndex == 1; - } - - private void setAutoSimError(boolean hasError) { - getSharedPreferences(PREFS_NAME, MODE_PRIVATE) - .edit() - .putBoolean(AUTO_SIM_ERROR_KEY, hasError) - .apply(); - } - - 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(int execMode) { - SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - int targetSim = prefs.getInt(TARGET_SIM_KEY, TARGET_SIM_AUTO); - - if (targetSim == TARGET_SIM_1) { - setAutoSimError(false); - return 0; - } - - if (targetSim == TARGET_SIM_2) { - setAutoSimError(false); - return 1; - } - - return resolveAutoSlotIndex(execMode); - } - - private int resolveAutoSlotIndex(int execMode) { - 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 slotIndex = SubscriptionManager.getSlotIndex(dataSubId); - - if (isValidSlotIndex(slotIndex)) { - setAutoSimError(false); - return slotIndex; - } - } catch (Exception ignored) { - } - } - - int slotIndexFromDumpsys = resolveSlotIndexFromDumpsys(execMode, dataSubId); - - if (isValidSlotIndex(slotIndexFromDumpsys)) { - setAutoSimError(false); - return slotIndexFromDumpsys; - } - - setAutoSimError(true); - return INVALID_SLOT_INDEX; - } - - private int resolveSlotIndexFromDumpsys(int execMode, 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; - - if (execMode == MODE_SHIZUKU) { - result = runCommandForResultWithShizuku(command); - } else if (execMode == MODE_ROOT) { - result = runCommandForResultWithRoot(command); - } else { - return INVALID_SLOT_INDEX; - } - - if (result.exitCode != 0) { - return INVALID_SLOT_INDEX; - } - - Integer slotIndex = extractFirstInt(result.stdout); - - if (slotIndex == null || !isValidSlotIndex(slotIndex)) { - return INVALID_SLOT_INDEX; - } - - return slotIndex; - } - - private int resolveSubIdFromDumpsys(int execMode, 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; - - if (execMode == MODE_SHIZUKU) { - result = runCommandForResultWithShizuku(command); - } else if (execMode == MODE_ROOT) { - result = runCommandForResultWithRoot(command); - } else { - return INVALID_SUB_ID; - } - - if (result.exitCode != 0) { - return INVALID_SUB_ID; - } - - Integer subId = extractFirstInt(result.stdout); - - if (subId == null || subId <= 0) { - return INVALID_SUB_ID; - } - - return subId; - } - - - private boolean applyNetworkMode(String binaryString) { - SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - int execMode = prefs.getInt(EXEC_MODE_KEY, MODE_NONE); - - if (execMode == MODE_NONE) { - return false; - } - - int slotIndex = resolveTargetSlotIndex(execMode); - - if (!isValidSlotIndex(slotIndex)) { - return false; - } - - String command = - "cmd phone set-allowed-network-types-for-users -s " + - slotIndex + - " " + - binaryString; - - if (execMode == MODE_SHIZUKU) { - return runCommandWithShizuku(command); - } else if (execMode == MODE_ROOT) { - return runCommandWithRoot(command); - } - - return false; - } - - - - private boolean runCommandWithRoot(String command) { - Process process = null; - - try { - process = Runtime.getRuntime().exec(new String[]{"su", "-c", command}); - int exitCode = process.waitFor(); - return exitCode == 0; - } catch (Exception e) { - e.printStackTrace(); - return false; - } finally { - if (process != null) { - process.destroy(); - } - } - } - - private boolean runCommandWithShizuku(String command) { - Process process = null; - - try { - if (!Shizuku.pingBinder()) { - return false; - } - - if (Shizuku.checkSelfPermission() != android.content.pm.PackageManager.PERMISSION_GRANTED) { - return false; - } - - process = (Process) getShizukuNewProcessMethod().invoke( - null, - new String[]{"sh", "-c", command}, - null, - null - ); - - if (process == null) { - return false; - } - - int exitCode = process.waitFor(); - return exitCode == 0; - - } catch (Exception e) { - e.printStackTrace(); - return false; - }finally { - if (process != null) { - process.destroy(); - } - } - } + + 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; + } + } + + private void updateTileSwitchingUI() { + Tile tile = getQsTile(); + if (tile == null) return; + tile.setState(Tile.STATE_INACTIVE); + tile.setLabel("Switching..."); + tile.setIcon(getCachedIcon("?")); + tile.updateTile(); + } + + private void updateTileUI(NetworkMode mode) { + Tile tile = getQsTile(); + if (tile == null) return; + if (mode == NetworkMode.UNKNOWN) { + if (appPreferences.getExecutionMode() == ExecutionMode.NONE) { + tile.setState(Tile.STATE_UNAVAILABLE); + tile.setLabel("Setup Required"); + } else { + tile.setState(Tile.STATE_INACTIVE); + tile.setLabel(mode.getTileLabel()); + } + tile.setIcon(getCachedIcon("?")); + } else { + tile.setState(Tile.STATE_ACTIVE); + 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.exitCode != 0) return NetworkMode.UNKNOWN; + return NetworkMode.fromLegacyMode(extractFirstInt(result.stdout)); + } + + private CommandResult runCommandForResult(ExecutionMode mode, String command) { + if (mode == ExecutionMode.SHIZUKU) return runCommandForResultWithShizuku(command); + if (mode == ExecutionMode.ROOT) return runCommandForResultWithRoot(command); + return new CommandResult(-1, ""); + } + + private CommandResult runCommandForResultWithRoot(String command) { + Process process = null; + try { + process = Runtime.getRuntime().exec(new String[]{"su", "-c", command}); + int exitCode = process.waitFor(); + return new CommandResult(exitCode, readStream(process.getInputStream())); + } catch (Exception e) { + return new CommandResult(-1, ""); + } finally { + if (process != null) process.destroy(); + } + } + + private CommandResult runCommandForResultWithShizuku(String command) { + Process process = null; + try { + if (!Shizuku.pingBinder() + || Shizuku.checkSelfPermission() != PackageManager.PERMISSION_GRANTED) { + return new CommandResult(-1, ""); + } + process = (Process) getShizukuNewProcessMethod().invoke( + null, new String[]{"sh", "-c", command}, null, null); + if (process == null) return new CommandResult(-1, ""); + int exitCode = process.waitFor(); + return new CommandResult(exitCode, readStream(process.getInputStream())); + } catch (Exception e) { + return new CommandResult(-1, ""); + } finally { + if (process != null) process.destroy(); + } + } + + private String readStream(InputStream stream) { + StringBuilder builder = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { + String line; + while ((line = reader.readLine()) != null) builder.append(line).append('\n'); + } catch (Exception ignored) { + } + return builder.toString().trim(); + } + + 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); + } + + 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); + } + + 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.exitCode == 0 ? extractFirstInt(result.stdout) : 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.exitCode == 0 ? extractFirstInt(result.stdout) : null; + return subId != null && subId > 0 ? subId : INVALID_SUB_ID; + } + + private boolean applyNetworkMode(NetworkMode mode, ExecutionMode executionMode) { + int slotIndex = resolveTargetSlotIndex(executionMode); + if (!isValidSlotIndex(slotIndex) || mode.getBinaryMask() == null) return false; + String command = "cmd phone set-allowed-network-types-for-users -s " + + slotIndex + " " + mode.getBinaryMask(); + if (executionMode == ExecutionMode.SHIZUKU) return runCommandWithShizuku(command); + if (executionMode == ExecutionMode.ROOT) return runCommandWithRoot(command); + return false; + } + + private boolean runCommandWithRoot(String command) { + Process process = null; + try { + process = Runtime.getRuntime().exec(new String[]{"su", "-c", command}); + return process.waitFor() == 0; + } catch (Exception e) { + return false; + } finally { + if (process != null) process.destroy(); + } + } + + private boolean runCommandWithShizuku(String command) { + Process process = null; + try { + if (!Shizuku.pingBinder() + || Shizuku.checkSelfPermission() != PackageManager.PERMISSION_GRANTED) return false; + process = (Process) getShizukuNewProcessMethod().invoke( + null, new String[]{"sh", "-c", command}, null, null); + return process != null && process.waitFor() == 0; + } catch (Exception e) { + return false; + } finally { + if (process != null) process.destroy(); + } + } } diff --git a/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java b/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java new file mode 100644 index 0000000..4b824fe --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java @@ -0,0 +1,81 @@ +package com.dhangofa.networktoggle.config; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.dhangofa.networktoggle.model.ExecutionMode; +import com.dhangofa.networktoggle.model.NetworkMode; +import com.dhangofa.networktoggle.model.TargetSim; + +public final class AppPreferences { + private static final String PREFS_NAME = "NetTogglePrefs"; + 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_AUTO_SIM_ERROR = "auto_sim_error"; + + private final SharedPreferences preferences; + + public AppPreferences(Context context) { + preferences = context.getApplicationContext() + .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + + public ExecutionMode getExecutionMode() { + return ExecutionMode.fromValue( + preferences.getInt(KEY_EXEC_MODE, ExecutionMode.NONE.getValue())); + } + + public void setExecutionMode(ExecutionMode mode) { + preferences.edit().putInt(KEY_EXEC_MODE, mode.getValue()).apply(); + } + + public TargetSim getTargetSim() { + return TargetSim.fromValue( + preferences.getInt(KEY_TARGET_SIM, TargetSim.AUTO.getValue())); + } + + public void setTargetSim(TargetSim targetSim) { + preferences.edit().putInt(KEY_TARGET_SIM, targetSim.getValue()).apply(); + } + + public NetworkMode getCachedNetworkMode() { + return NetworkMode.fromStateValue( + preferences.getInt(KEY_NETWORK_STATE, NetworkMode.UNKNOWN.getStateValue())); + } + + public void setCachedNetworkMode(NetworkMode mode) { + preferences.edit().putInt(KEY_NETWORK_STATE, mode.getStateValue()).apply(); + } + + public boolean hasAutoSimError() { + return preferences.getBoolean(KEY_AUTO_SIM_ERROR, false); + } + + public void setAutoSimError(boolean hasError) { + preferences.edit().putBoolean(KEY_AUTO_SIM_ERROR, hasError).apply(); + } + + public void onExecutionModeChanged(ExecutionMode mode) { + preferences.edit() + .putInt(KEY_EXEC_MODE, mode.getValue()) + .putInt(KEY_NETWORK_STATE, NetworkMode.UNKNOWN.getStateValue()) + .putBoolean(KEY_AUTO_SIM_ERROR, false) + .apply(); + } + + public void onTargetSimChanged(TargetSim targetSim) { + preferences.edit() + .putInt(KEY_TARGET_SIM, targetSim.getValue()) + .putInt(KEY_NETWORK_STATE, NetworkMode.UNKNOWN.getStateValue()) + .putBoolean(KEY_AUTO_SIM_ERROR, false) + .apply(); + } + + public void clearTransientState() { + preferences.edit() + .putInt(KEY_NETWORK_STATE, NetworkMode.UNKNOWN.getStateValue()) + .putBoolean(KEY_AUTO_SIM_ERROR, false) + .apply(); + } +} diff --git a/app/src/main/java/com/dhangofa/networktoggle/model/ExecutionMode.java b/app/src/main/java/com/dhangofa/networktoggle/model/ExecutionMode.java new file mode 100644 index 0000000..3270e68 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/model/ExecutionMode.java @@ -0,0 +1,26 @@ +package com.dhangofa.networktoggle.model; + +public enum ExecutionMode { + NONE(0), + ROOT(1), + SHIZUKU(2); + + private final int value; + + ExecutionMode(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static ExecutionMode fromValue(int value) { + for (ExecutionMode mode : values()) { + if (mode.value == value) { + return mode; + } + } + return NONE; + } +} diff --git a/app/src/main/java/com/dhangofa/networktoggle/model/NetworkMode.java b/app/src/main/java/com/dhangofa/networktoggle/model/NetworkMode.java new file mode 100644 index 0000000..4e9b10c --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/model/NetworkMode.java @@ -0,0 +1,69 @@ +package com.dhangofa.networktoggle.model; + +public enum NetworkMode { + UNKNOWN(0, "Unknown", "Tap to Set 4G", "?", null), + FOUR_G_ONLY(1, "4G Only", "4G Only", "4G", "1000000000000"), + FIVE_G_ONLY(2, "5G Only", "5G Only", "5G", "10000000000000000000"), + PREFERRED_5G(3, "Preferred 5G", "Pref 5G", "P5G", "11011111101111111111"), + PREFERRED_4G(4, "Preferred 4G", "Pref 4G", "P4G", "1001101001110000111"); + + private final int stateValue; + private final String displayName; + private final String tileLabel; + private final String iconText; + private final String binaryMask; + + NetworkMode(int stateValue, String displayName, String tileLabel, String iconText, String binaryMask) { + this.stateValue = stateValue; + this.displayName = displayName; + this.tileLabel = tileLabel; + this.iconText = iconText; + this.binaryMask = binaryMask; + } + + public int getStateValue() { return stateValue; } + public String getDisplayName() { return displayName; } + public String getTileLabel() { return tileLabel; } + public String getIconText() { return iconText; } + public String getBinaryMask() { return binaryMask; } + + public static NetworkMode fromStateValue(int value) { + for (NetworkMode mode : values()) { + if (mode.stateValue == value) return mode; + } + return UNKNOWN; + } + + public static NetworkMode nextInDefaultCycle(NetworkMode current) { + switch (current) { + case FOUR_G_ONLY: return FIVE_G_ONLY; + case FIVE_G_ONLY: return PREFERRED_5G; + case PREFERRED_5G: return PREFERRED_4G; + case PREFERRED_4G: + case UNKNOWN: + default: return FOUR_G_ONLY; + } + } + + public static NetworkMode fromLegacyMode(Integer legacyMode) { + if (legacyMode == null) return UNKNOWN; + switch (legacyMode) { + case 11: return FOUR_G_ONLY; + case 23: return FIVE_G_ONLY; + case 33: return PREFERRED_5G; + case 9: + case 8: + case 10: + case 12: + case 15: + case 17: + case 19: + case 20: + case 22: + return PREFERRED_4G; + default: + if (legacyMode >= 24 && legacyMode <= 32) return PREFERRED_5G; + return UNKNOWN; + } + } +} diff --git a/app/src/main/java/com/dhangofa/networktoggle/model/TargetSim.java b/app/src/main/java/com/dhangofa/networktoggle/model/TargetSim.java new file mode 100644 index 0000000..cfdfffa --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/model/TargetSim.java @@ -0,0 +1,36 @@ +package com.dhangofa.networktoggle.model; + +public enum TargetSim { + AUTO(0, -1), + SIM_1(1, 0), + SIM_2(2, 1); + + private final int value; + private final int manualSlotIndex; + + TargetSim(int value, int manualSlotIndex) { + this.value = value; + this.manualSlotIndex = manualSlotIndex; + } + + public int getValue() { + return value; + } + + public boolean isAuto() { + return this == AUTO; + } + + public int getManualSlotIndex() { + return manualSlotIndex; + } + + public static TargetSim fromValue(int value) { + for (TargetSim target : values()) { + if (target.value == value) { + return target; + } + } + return AUTO; + } +}