diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 22a5513..6726641 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,8 +10,8 @@ android { applicationId = "com.dhangofa.networktoggle" minSdk = 24 targetSdk = 36 - versionCode = 32 - versionName = "1.0.32" + versionCode = 65 + versionName = "1.0.65" } // Suggested by IzzyOnDroid dependenciesInfo { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f4291a9..70dc52b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + { + new Thread(() -> { + NetworkMode currentMode = appPreferences.getCachedNetworkMode(); + + // If cache is wiped (e.g. from SIM switch), but execution is allowed, read it directly once + if (currentMode == NetworkMode.UNKNOWN && appPreferences.getExecutionMode() != ExecutionMode.NONE) { + currentMode = new com.dhangofa.networktoggle.telephony.NetworkModeReader(this, appPreferences, simResolver).readCurrentMode(); + } + + if (currentMode != NetworkMode.UNKNOWN && !newCycle.contains(currentMode)) { + NetworkMode fallbackMode = newCycle.get(0); + // Attempt to sync the network state silently + modeController.apply(fallbackMode, appPreferences.getExecutionMode()); + appPreferences.setCachedNetworkMode(fallbackMode); + } else if (currentMode != NetworkMode.UNKNOWN) { + // It's in the cycle, ensure it is cached so UI shows the actual active state + appPreferences.setCachedNetworkMode(currentMode); + } + }).start(); + }); + tileCycleUiController.initialize(); registerShizukuListeners(); loadSavedExecutionMode(); loadSavedTargetSimMode(); updateAutoSimWarning(); bindSelectionListeners(); + updateSeparatorVisibility(); } private void configureStatusBar() { @@ -118,7 +171,7 @@ private void configureStatusBar() { boolean isNight = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; - getWindow().setStatusBarColor(getColor(R.color.surface_background)); + getWindow().setStatusBarColor(getColor(R.color.card_surface)); getWindow().getDecorView().setSystemUiVisibility( isNight ? 0 : View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); } @@ -136,11 +189,47 @@ private void bindViews() { autoSimWarningText = findViewById(R.id.autoSimWarningText); githubLink = findViewById(R.id.githubLink); telegramLink = findViewById(R.id.telegramLink); + separatorRootShizuku = findViewById(R.id.separatorRootShizuku); + separatorAutoSim1 = findViewById(R.id.separatorAutoSim1); + separatorSim1Sim2 = findViewById(R.id.separatorSim1Sim2); + + errorBannerContainer = findViewById(R.id.cardErrorBanner); + if (errorBannerContainer != null) { + errorBannerContainer.setOnClickListener(v -> showDiagnosticDialog()); + } + + } private void bindLinks() { githubLink.setOnClickListener(v -> openUrl("https://github.com/Dhangofa/NetToggle")); - telegramLink.setOnClickListener(v -> openUrl("https://t.me/dhangofa")); + telegramLink.setOnClickListener(v -> openUrl("https://t.me/dhangofas_projects_chat")); + } + + private void updateSeparatorVisibility() { + // Execution Mode Separator + int modeId = radioGroup.getCheckedRadioButtonId(); + if (modeId == -1) { + separatorRootShizuku.setVisibility(View.VISIBLE); + } else { + separatorRootShizuku.setVisibility(View.INVISIBLE); + } + + // Target SIM Separators + int simId = targetSimRadioGroup.getCheckedRadioButtonId(); + if (simId == -1) { + separatorAutoSim1.setVisibility(View.VISIBLE); + separatorSim1Sim2.setVisibility(View.VISIBLE); + } else if (simId == R.id.radioSimAuto) { + separatorAutoSim1.setVisibility(View.INVISIBLE); + separatorSim1Sim2.setVisibility(View.VISIBLE); + } else if (simId == R.id.radioSim1) { + separatorAutoSim1.setVisibility(View.INVISIBLE); + separatorSim1Sim2.setVisibility(View.INVISIBLE); + } else if (simId == R.id.radioSim2) { + separatorAutoSim1.setVisibility(View.VISIBLE); + separatorSim1Sim2.setVisibility(View.INVISIBLE); + } } private void registerShizukuListeners() { @@ -163,8 +252,11 @@ private void loadSavedExecutionMode() { } } + private boolean updatingSimUi = false; + private void bindSelectionListeners() { radioGroup.setOnCheckedChangeListener((group, checkedId) -> { + updateSeparatorVisibility(); if (checkedId == R.id.radioRoot) { appPreferences.onExecutionModeChanged(ExecutionMode.ROOT); checkRootPermission(); @@ -174,12 +266,51 @@ private void bindSelectionListeners() { } }); + android.view.View.OnTouchListener lockTouch = (v, event) -> { + if (!isUIAuthorized && event.getAction() == android.view.MotionEvent.ACTION_DOWN) { + android.widget.Toast.makeText(MainActivity.this, "Please authorize Root or Shizuku to configure toggles.", android.widget.Toast.LENGTH_SHORT).show(); + return true; + } + return false; + }; + radioSimAuto.setOnTouchListener(lockTouch); + radioSim1.setOnTouchListener(lockTouch); + radioSim2.setOnTouchListener(lockTouch); + targetSimRadioGroup.setOnCheckedChangeListener((group, checkedId) -> { + if (updatingSimUi) return; TargetSim target = TargetSim.AUTO; if (checkedId == R.id.radioSim1) target = TargetSim.SIM_1; else if (checkedId == R.id.radioSim2) target = TargetSim.SIM_2; + + // Validate slot immediately + if (target != TargetSim.AUTO && checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) { + android.telephony.SubscriptionManager sm = getSystemService(android.telephony.SubscriptionManager.class); + if (sm != null) { + boolean found = false; + java.util.List infos = sm.getActiveSubscriptionInfoList(); + if (infos != null) { + for (android.telephony.SubscriptionInfo info : infos) { + if (info.getSimSlotIndex() == target.getManualSlotIndex()) { + found = true; + break; + } + } + } + if (!found) { + Toast.makeText(MainActivity.this, "No SIM card found in slot " + (target.getManualSlotIndex() + 1), Toast.LENGTH_SHORT).show(); + updatingSimUi = true; + radioSimAuto.setChecked(true); + updatingSimUi = false; + target = TargetSim.AUTO; + } + } + } + + updateSeparatorVisibility(); appPreferences.onTargetSimChanged(target); updateAutoSimWarning(); + updateCapabilities(); }); } @@ -187,11 +318,125 @@ private void bindSelectionListeners() { protected void onResume() { super.onResume(); if (appPreferences != null) updateAutoSimWarning(); + checkAndRequestPermission(); + updateErrorBanner(); + updateCapabilities(); + } + + private void checkAndRequestPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + boolean granted = checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED; + if (!granted) { + boolean shouldShowRationale = shouldShowRequestPermissionRationale(android.Manifest.permission.READ_PHONE_STATE); + if (shouldShowRationale) { + showPermissionBottomSheet(); + } else { + // Automatically prompt on first open if not asked yet + requestPermissions(new String[]{android.Manifest.permission.READ_PHONE_STATE}, REQ_CODE_PHONE_STATE); + } + } else if (permissionDialog != null && permissionDialog.isShowing()) { + permissionDialog.dismiss(); + } + } + } + + private void showPermissionBottomSheet() { + if (permissionDialog == null) { + permissionDialog = new Dialog(this, R.style.TransparentBottomSheetStyle); + View view = getLayoutInflater().inflate(R.layout.bottom_sheet_permission, null); + + view.findViewById(R.id.btnDismissPermission).setOnClickListener(v -> permissionDialog.dismiss()); + view.findViewById(R.id.btnGrantPermission).setOnClickListener(v -> { + permissionDialog.dismiss(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions(new String[]{android.Manifest.permission.READ_PHONE_STATE}, REQ_CODE_PHONE_STATE); + } + }); + + permissionDialog.setContentView(view); + Window window = permissionDialog.getWindow(); + if (window != null) { + window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + window.setGravity(Gravity.BOTTOM); + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + } + } + + if (!permissionDialog.isShowing() && !activityDestroyed) { + permissionDialog.show(); + } + } + + private void updateErrorBanner() { + if (errorBannerContainer != null && appPreferences != null) { + DiagnosticError error = appPreferences.getLastError(); + errorBannerContainer.setVisibility(error != null ? View.VISIBLE : View.GONE); + } + } + + private void showDiagnosticDialog() { + if (isFinishing() || activityDestroyed) return; + + if (diagnosticDialog != null && diagnosticDialog.isShowing()) { + diagnosticDialog.dismiss(); appPreferences.clearLastError(); updateErrorBanner(); + } + + diagnosticDialog = new Dialog(this); + diagnosticDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + diagnosticDialog.setContentView(R.layout.dialog_diagnostic); + diagnosticDialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + diagnosticDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + TextView reportText = diagnosticDialog.findViewById(R.id.diagnosticReportText); + if (reportText != null) { + String report = DiagnosticReporter.generateReport(appPreferences, new SimResolver(this, appPreferences)); + reportText.setText(report); + } + + View btnClose = diagnosticDialog.findViewById(R.id.btnCloseDiagnostic); + if (btnClose != null) btnClose.setOnClickListener(v -> { diagnosticDialog.dismiss(); appPreferences.clearLastError(); updateErrorBanner(); }); + + View btnCopy = diagnosticDialog.findViewById(R.id.btnCopyDiagnostic); + if (btnCopy != null) { + btnCopy.setOnClickListener(v -> { + ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Diagnostic Report", reportText.getText()); + clipboard.setPrimaryClip(clip); + Toast.makeText(this, "Report copied to clipboard", Toast.LENGTH_SHORT).show(); + }); + } + + diagnosticDialog.show(); + } + + private void updateCapabilities() { + new Thread(() -> { + AppPreferences.NetworkCapabilities caps = capabilityResolver.getCapabilities(appPreferences.getExecutionMode()); + runOnUiThread(() -> { + if (!activityDestroyed && tileCycleUiController != null) { + tileCycleUiController.applyCapabilities(caps); + } + }); + }).start(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == REQ_CODE_PHONE_STATE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (permissionDialog != null && permissionDialog.isShowing()) permissionDialog.dismiss(); + updateCapabilities(); // Fetch capabilities immediately upon grant + } else { + showPermissionBottomSheet(); + } + } } @Override protected void onDestroy() { activityDestroyed = true; + if (appPreferences != null) appPreferences.unregisterListener(this); Shizuku.removeBinderReceivedListener(binderReceivedListener); Shizuku.removeBinderDeadListener(binderDeadListener); Shizuku.removeRequestPermissionResultListener(permissionResultListener); @@ -207,6 +452,13 @@ protected void onDestroy() { super.onDestroy(); } + @Override + public void onSharedPreferenceChanged(android.content.SharedPreferences sharedPreferences, String key) { + if ("last_error_cmd".equals(key)) { + runOnUiThread(() -> updateErrorBanner()); + } + } + private void checkRootPermission() { setStatus("Checking root permission...", 0xFFFFB300); rootCheckThread = new Thread(() -> { @@ -227,8 +479,14 @@ private void checkRootPermission() { 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); + if (finalGranted) { + setStatus("Root mode active & authorized!", 0xFF1B873F); + appPreferences.setTileErrorState(com.dhangofa.networktoggle.config.AppPreferences.TILE_ERROR_NONE); + } else { + setStatus("Root permission denied or unavailable.", 0xFFFF5555); + appPreferences.setTileErrorState(com.dhangofa.networktoggle.config.AppPreferences.TILE_ERROR_ROOT); + } + android.service.quicksettings.TileService.requestListeningState(MainActivity.this, new android.content.ComponentName(MainActivity.this, NetworkTileService.class)); }); }); rootCheckThread.start(); @@ -239,19 +497,35 @@ private void checkShizukuPermission(boolean requestIfNeeded) { try { if (!Shizuku.pingBinder()) { setStatus("Shizuku is not running.", 0xFFFF5555); + if (appPreferences != null) { + appPreferences.setTileErrorState(com.dhangofa.networktoggle.config.AppPreferences.TILE_ERROR_SHIZUKU); + android.service.quicksettings.TileService.requestListeningState(MainActivity.this, new android.content.ComponentName(MainActivity.this, NetworkTileService.class)); + } return; } if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { setStatus("Shizuku mode active & authorized!", 0xFF1B873F); + if (appPreferences != null) { + appPreferences.setTileErrorState(com.dhangofa.networktoggle.config.AppPreferences.TILE_ERROR_NONE); + android.service.quicksettings.TileService.requestListeningState(MainActivity.this, new android.content.ComponentName(MainActivity.this, NetworkTileService.class)); + } return; } setStatus("Shizuku permission not granted.", 0xFFFFB300); + if (appPreferences != null) { + appPreferences.setTileErrorState(com.dhangofa.networktoggle.config.AppPreferences.TILE_ERROR_SHIZUKU); + android.service.quicksettings.TileService.requestListeningState(MainActivity.this, new android.content.ComponentName(MainActivity.this, NetworkTileService.class)); + } if (requestIfNeeded) { setStatus("Requesting Shizuku permission...", 0xFFFFB300); Shizuku.requestPermission(0); } } catch (Exception e) { setStatus("Shizuku check failed.", 0xFFFF5555); + if (appPreferences != null) { + appPreferences.setTileErrorState(com.dhangofa.networktoggle.config.AppPreferences.TILE_ERROR_SHIZUKU); + android.service.quicksettings.TileService.requestListeningState(MainActivity.this, new android.content.ComponentName(MainActivity.this, NetworkTileService.class)); + } } } @@ -272,6 +546,32 @@ private void setStatus(String text, int color) { R.drawable.shape_pill_badge_bg ); } + updateAuthorizationUI(color == 0xFF1B873F); + } + + private boolean isUIAuthorized = true; + private void updateAuthorizationUI(boolean authorized) { + if (isUIAuthorized == authorized) return; + isUIAuthorized = authorized; + + float alpha = authorized ? 1.0f : 0.4f; + View targetSimCard = findViewById(R.id.targetSimRadioGroup); + if (targetSimCard != null) targetSimCard.setAlpha(alpha); + View targetSimHeader = findViewById(R.id.targetSimHeaderContainer); + if (targetSimHeader != null) targetSimHeader.setAlpha(alpha); + View autoSimWarningText = findViewById(R.id.autoSimWarningText); + if (autoSimWarningText != null) autoSimWarningText.setAlpha(alpha); + + if (tileCycleUiController != null) { + tileCycleUiController.setAuthorized(authorized); + } + + if (authorized && appPreferences != null) { + appPreferences.clearDeviceCapabilities(); + appPreferences.invalidateSlotCache(0); + appPreferences.invalidateSlotCache(1); + updateCapabilities(); + } } private String getAppVersionName() { diff --git a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java index 5d62579..6579114 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java +++ b/app/src/main/java/com/dhangofa/networktoggle/NetworkTileService.java @@ -1,4 +1,5 @@ package com.dhangofa.networktoggle; +import android.os.Build; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -10,6 +11,10 @@ import android.os.Looper; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; +import android.content.Intent; +import android.app.PendingIntent; +import rikka.shizuku.Shizuku; +import android.content.pm.PackageManager; import android.widget.Toast; import com.dhangofa.networktoggle.config.AppPreferences; @@ -35,6 +40,8 @@ public class NetworkTileService extends TileService { private static Icon icon5g; private static Icon iconP5g; private static Icon iconP4g; + private static Icon iconP3g; + private static Icon icon2g; private static Icon iconUnknown; private final Handler mainHandler = new Handler(Looper.getMainLooper()); @@ -42,22 +49,41 @@ public class NetworkTileService extends TileService { private AppPreferences appPreferences; private NetworkModeReader networkModeReader; private NetworkModeController networkModeController; - private TileCycleManager tileCycleManager; + private TileCycleManager tileCycleManager; + private com.dhangofa.networktoggle.telephony.SimResolver simResolver; @Override public void onCreate() { super.onCreate(); + // Init dependencies appPreferences = new AppPreferences(this); - tileCycleManager = new TileCycleManager(appPreferences); - SimResolver simResolver = new SimResolver(appPreferences); - networkModeReader = new NetworkModeReader(appPreferences, simResolver); + tileCycleManager = new TileCycleManager(appPreferences); + simResolver = new com.dhangofa.networktoggle.telephony.SimResolver(this, appPreferences); + networkModeReader = new com.dhangofa.networktoggle.telephony.NetworkModeReader(this, appPreferences, simResolver); networkModeController = new NetworkModeController(simResolver); } @Override public void onStartListening() { super.onStartListening(); + + // Passive Shizuku Check + if (appPreferences.getExecutionMode() == ExecutionMode.SHIZUKU) { + boolean isShizukuOk = false; + try { + isShizukuOk = rikka.shizuku.Shizuku.pingBinder() && rikka.shizuku.Shizuku.checkSelfPermission() == android.content.pm.PackageManager.PERMISSION_GRANTED; + } catch (Throwable t) { + isShizukuOk = false; + } + + int currentError = appPreferences.getTileErrorState(); + if (!isShizukuOk && currentError != AppPreferences.TILE_ERROR_SHIZUKU) { + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_SHIZUKU); + } else if (isShizukuOk && currentError == AppPreferences.TILE_ERROR_SHIZUKU) { + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_NONE); + } + } NetworkMode cachedMode = appPreferences.getCachedNetworkMode(); updateTileUI(cachedMode); @@ -67,25 +93,25 @@ public void onStartListening() { return; } - EXECUTOR.execute(() -> { - NetworkMode realMode = networkModeReader.readCurrentMode(); + EXECUTOR.execute(() -> { + NetworkMode realMode = networkModeReader.readCurrentMode(); - mainHandler.post(() -> { - /* - * A tile click or another operation may have updated the state - * while this asynchronous readback was running. - */ - if (appPreferences.getCachedNetworkMode() - != NetworkMode.UNKNOWN) { - return; - } + mainHandler.post(() -> { + /* + * A tile click or another operation may have updated the state + * while this asynchronous readback was running. + */ + if (appPreferences.getCachedNetworkMode() + != NetworkMode.UNKNOWN) { + return; + } - if (realMode != NetworkMode.UNKNOWN) { - appPreferences.setCachedNetworkMode(realMode); - updateTileUI(realMode); - } + if (realMode != NetworkMode.UNKNOWN) { + appPreferences.setCachedNetworkMode(realMode); + updateTileUI(realMode); + } + }); }); - }); } @Override @@ -109,27 +135,70 @@ public void onClick() { updateTileSwitchingUI(); EXECUTOR.execute(() -> { + int slotIndex = simResolver.resolveTargetSlotIndex(executionMode); + if (!simResolver.isValidSlotIndex(slotIndex)) { + mainHandler.post(() -> { + Toast.makeText(getApplicationContext(), "No SIM card found in target slot.", Toast.LENGTH_SHORT).show(); + appPreferences.onTargetSimChanged(com.dhangofa.networktoggle.model.TargetSim.AUTO); + updateTileUI(appPreferences.getCachedNetworkMode()); + IS_SWITCHING.set(false); + }); + return; + } + CommandResult result = networkModeController.apply( nextMode, executionMode ); - mainHandler.post(() -> { - try { - if (result.isSuccess()) { - appPreferences.setCachedNetworkMode(nextMode); - appPreferences.setAutoSimError(false); - updateTileUI(nextMode); - } else { - updateTileUI(currentMode); - if (appPreferences.hasAutoSimError()) { - showAutoSimErrorToast(); + if (result.isSuccess()) { + appPreferences.setCachedNetworkMode(nextMode); + appPreferences.setAutoSimError(false); + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_NONE); + mainHandler.post(() -> { + updateTileUI(nextMode); + IS_SWITCHING.set(false); + }); + } else { + // Command failed! Check for permission failures first. + boolean isAuthError = false; + if (executionMode == ExecutionMode.SHIZUKU) { + try { + if (!Shizuku.pingBinder() || Shizuku.checkSelfPermission() != PackageManager.PERMISSION_GRANTED) { + isAuthError = true; + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_SHIZUKU); } + } catch (Throwable t) { + isAuthError = true; + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_SHIZUKU); + } + } else if (executionMode == ExecutionMode.ROOT) { + try { + Process p = Runtime.getRuntime().exec(new String[]{"su", "-c", "true"}); + int exitCode = p.waitFor(); + if (exitCode != 0) { + isAuthError = true; + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_ROOT); + } + } catch (Exception e) { + isAuthError = true; + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_ROOT); } - } finally { - IS_SWITCHING.set(false); } - }); + + if (!isAuthError) { + appPreferences.setTileErrorState(AppPreferences.TILE_ERROR_CMD); + appPreferences.setLastError(result.getCommand(), result.getStderr()); + } + + mainHandler.post(() -> { + updateTileUI(currentMode); + if (appPreferences.hasAutoSimError()) { + showAutoSimErrorToast(); + } + IS_SWITCHING.set(false); + }); + } }); } @@ -177,6 +246,12 @@ private Icon getCachedIcon(String text) { case "P4G": if (iconP4g == null) iconP4g = createTextOnlyIcon("P4G"); return iconP4g; + case "P3G": + if (iconP3g == null) iconP3g = createTextOnlyIcon("P3G"); + return iconP3g; + case "2G": + if (icon2g == null) icon2g = createTextOnlyIcon("2G"); + return icon2g; default: if (iconUnknown == null) { iconUnknown = createTextOnlyIcon("?"); @@ -202,6 +277,27 @@ private void updateTileUI(NetworkMode mode) { return; } + int errorState = appPreferences.getTileErrorState(); + if (errorState == AppPreferences.TILE_ERROR_SHIZUKU) { + tile.setState(Tile.STATE_UNAVAILABLE); + tile.setLabel("Shizuku Unavailable"); + tile.setIcon(getCachedIcon("?")); + tile.updateTile(); + return; + } else if (errorState == AppPreferences.TILE_ERROR_ROOT) { + tile.setState(Tile.STATE_UNAVAILABLE); + tile.setLabel("Root Unavailable"); + tile.setIcon(getCachedIcon("?")); + tile.updateTile(); + return; + } else if (errorState == AppPreferences.TILE_ERROR_CMD) { + tile.setState(Tile.STATE_INACTIVE); + tile.setLabel("Error Check App"); + tile.setIcon(getCachedIcon("?")); + tile.updateTile(); + return; + } + if (mode == NetworkMode.UNKNOWN) { if (appPreferences.getExecutionMode() == ExecutionMode.NONE) { tile.setState(Tile.STATE_UNAVAILABLE); @@ -223,6 +319,8 @@ private void updateTileUI(NetworkMode mode) { tile.updateTile(); } + + private void showAutoSimErrorToast() { Toast.makeText( this, diff --git a/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java b/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java index de5ac8f..fdc95fb 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java +++ b/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java @@ -17,6 +17,21 @@ public final class AppPreferences { private static final String KEY_NETWORK_STATE = "net_state"; private static final String KEY_AUTO_SIM_ERROR = "auto_sim_error"; private static final String KEY_TILE_CYCLE_MODES = "tile_cycle_modes"; + + private static final String KEY_LAST_ERROR_CMD = "last_error_cmd"; + private static final String KEY_LAST_ERROR_STDERR = "last_error_stderr"; + private static final String KEY_LAST_ERROR_TIMESTAMP = "last_error_time"; + + // Keys for capabilities caching + private static final String KEY_DEVICE_CAPS_PREFIX = "device_cap_"; + private static final String KEY_SLOT_SUBID_PREFIX = "slot_subid_"; + private static final String KEY_SLOT_CAPS_PREFIX = "slot_cap_"; + + public static final int TILE_ERROR_NONE = 0; + public static final int TILE_ERROR_SHIZUKU = 1; + public static final int TILE_ERROR_ROOT = 2; + public static final int TILE_ERROR_CMD = 3; + private static final String KEY_TILE_ERROR = "tile_error_state"; private final SharedPreferences preferences; @@ -25,6 +40,22 @@ public AppPreferences(Context context) { .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); } + public int getTileErrorState() { + return preferences.getInt(KEY_TILE_ERROR, TILE_ERROR_NONE); + } + + public void setTileErrorState(int state) { + preferences.edit().putInt(KEY_TILE_ERROR, state).apply(); + } + + public void registerListener(SharedPreferences.OnSharedPreferenceChangeListener listener) { + preferences.registerOnSharedPreferenceChangeListener(listener); + } + + public void unregisterListener(SharedPreferences.OnSharedPreferenceChangeListener listener) { + preferences.unregisterOnSharedPreferenceChangeListener(listener); + } + public ExecutionMode getExecutionMode() { return ExecutionMode.fromValue( preferences.getInt(KEY_EXEC_MODE, ExecutionMode.NONE.getValue())); @@ -56,7 +87,36 @@ public void setAutoSimError(boolean hasError) { preferences.edit().putBoolean(KEY_AUTO_SIM_ERROR, hasError).apply(); } + public void setLastError(String command, String stderr) { + preferences.edit() + .putString(KEY_LAST_ERROR_CMD, command) + .putString(KEY_LAST_ERROR_STDERR, stderr) + .putLong(KEY_LAST_ERROR_TIMESTAMP, System.currentTimeMillis()) + .apply(); + } + + public com.dhangofa.networktoggle.model.DiagnosticError getLastError() { + String cmd = preferences.getString(KEY_LAST_ERROR_CMD, null); + String stderr = preferences.getString(KEY_LAST_ERROR_STDERR, null); + long time = preferences.getLong(KEY_LAST_ERROR_TIMESTAMP, 0); + + if (cmd == null && stderr == null) return null; + return new com.dhangofa.networktoggle.model.DiagnosticError(cmd, stderr, time); + } + + public void clearLastError() { + if (getTileErrorState() == TILE_ERROR_CMD) { + setTileErrorState(TILE_ERROR_NONE); + } + preferences.edit() + .remove(KEY_LAST_ERROR_CMD) + .remove(KEY_LAST_ERROR_STDERR) + .remove(KEY_LAST_ERROR_TIMESTAMP) + .apply(); + } + public void onExecutionModeChanged(ExecutionMode mode) { + setTileErrorState(TILE_ERROR_NONE); preferences.edit() .putInt(KEY_EXEC_MODE, mode.getValue()) .putInt(KEY_NETWORK_STATE, NetworkMode.UNKNOWN.getStateValue()) @@ -103,4 +163,91 @@ public void clearTransientState() { .putBoolean(KEY_AUTO_SIM_ERROR, false) .apply(); } + + // --- CAPABILITY CACHING LOGIC --- + + public static class NetworkCapabilities { + public final boolean supports2g; + public final boolean supports3g; + public final boolean supports4g; + public final boolean supports5g; + + public NetworkCapabilities(boolean supports2g, boolean supports3g, boolean supports4g, boolean supports5g) { + this.supports2g = supports2g; + this.supports3g = supports3g; + this.supports4g = supports4g; + this.supports5g = supports5g; + } + + // Failsafe fallback: Assume everything is supported if we can't fetch it + public static NetworkCapabilities assumeAll() { + return new NetworkCapabilities(true, true, true, true); + } + } + + public void saveDeviceCapabilities(NetworkCapabilities caps) { + preferences.edit() + .putBoolean(KEY_DEVICE_CAPS_PREFIX + "2g", caps.supports2g) + .putBoolean(KEY_DEVICE_CAPS_PREFIX + "3g", caps.supports3g) + .putBoolean(KEY_DEVICE_CAPS_PREFIX + "4g", caps.supports4g) + .putBoolean(KEY_DEVICE_CAPS_PREFIX + "5g", caps.supports5g) + .apply(); + } + + public NetworkCapabilities getDeviceCapabilities() { + if (!preferences.contains(KEY_DEVICE_CAPS_PREFIX + "5g")) { + return null; // Return null so the resolver knows it needs to fetch them + } + return new NetworkCapabilities( + preferences.getBoolean(KEY_DEVICE_CAPS_PREFIX + "2g", true), + preferences.getBoolean(KEY_DEVICE_CAPS_PREFIX + "3g", true), + preferences.getBoolean(KEY_DEVICE_CAPS_PREFIX + "4g", true), + preferences.getBoolean(KEY_DEVICE_CAPS_PREFIX + "5g", true) + ); + } + + public void saveSlotCapabilities(int slotIndex, int subId, NetworkCapabilities caps) { + preferences.edit() + .putInt(KEY_SLOT_SUBID_PREFIX + slotIndex, subId) + .putBoolean(KEY_SLOT_CAPS_PREFIX + slotIndex + "_2g", caps.supports2g) + .putBoolean(KEY_SLOT_CAPS_PREFIX + slotIndex + "_3g", caps.supports3g) + .putBoolean(KEY_SLOT_CAPS_PREFIX + slotIndex + "_4g", caps.supports4g) + .putBoolean(KEY_SLOT_CAPS_PREFIX + slotIndex + "_5g", caps.supports5g) + .apply(); + } + + public int getCachedSubIdForSlot(int slotIndex) { + return preferences.getInt(KEY_SLOT_SUBID_PREFIX + slotIndex, -1); + } + + public NetworkCapabilities getSlotCapabilities(int slotIndex) { + if (!preferences.contains(KEY_SLOT_CAPS_PREFIX + slotIndex + "_5g")) { + return null; + } + return new NetworkCapabilities( + preferences.getBoolean(KEY_SLOT_CAPS_PREFIX + slotIndex + "_2g", true), + preferences.getBoolean(KEY_SLOT_CAPS_PREFIX + slotIndex + "_3g", true), + preferences.getBoolean(KEY_SLOT_CAPS_PREFIX + slotIndex + "_4g", true), + preferences.getBoolean(KEY_SLOT_CAPS_PREFIX + slotIndex + "_5g", true) + ); + } + + public void clearDeviceCapabilities() { + preferences.edit() + .remove(KEY_DEVICE_CAPS_PREFIX + "2g") + .remove(KEY_DEVICE_CAPS_PREFIX + "3g") + .remove(KEY_DEVICE_CAPS_PREFIX + "4g") + .remove(KEY_DEVICE_CAPS_PREFIX + "5g") + .apply(); + } + + public void invalidateSlotCache(int slotIndex) { + preferences.edit() + .remove(KEY_SLOT_SUBID_PREFIX + slotIndex) + .remove(KEY_SLOT_CAPS_PREFIX + slotIndex + "_2g") + .remove(KEY_SLOT_CAPS_PREFIX + slotIndex + "_3g") + .remove(KEY_SLOT_CAPS_PREFIX + slotIndex + "_4g") + .remove(KEY_SLOT_CAPS_PREFIX + slotIndex + "_5g") + .apply(); + } } diff --git a/app/src/main/java/com/dhangofa/networktoggle/cycle/TileCycleManager.java b/app/src/main/java/com/dhangofa/networktoggle/cycle/TileCycleManager.java index 95155b1..2c007d1 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/cycle/TileCycleManager.java +++ b/app/src/main/java/com/dhangofa/networktoggle/cycle/TileCycleManager.java @@ -71,10 +71,39 @@ public ChangeResult setSelected(NetworkMode mode, boolean selected) { } appPreferences.setTileCycleModes(cycle); - appPreferences.clearCachedNetworkMode(); + // Cache is purposefully kept alive so UI changes can sync network state return ChangeResult.CHANGED; } + public boolean forceRemoveUnsupportedAndAutoFill(AppPreferences.NetworkCapabilities caps) { + List cycle = getCycle(); + boolean changed = false; + + if (!caps.supports5g) { + changed |= cycle.remove(NetworkMode.PREFERRED_5G); + changed |= cycle.remove(NetworkMode.FIVE_G_ONLY); + } + if (!caps.supports3g) { + changed |= cycle.remove(NetworkMode.PREFERRED_3G); + } + if (!caps.supports2g) { + changed |= cycle.remove(NetworkMode.TWO_G_ONLY); + } + + if (changed) { + // Auto fill to reach MIN_MODES + if (cycle.size() < MIN_MODES) { + if (!cycle.contains(NetworkMode.PREFERRED_4G)) cycle.add(NetworkMode.PREFERRED_4G); + if (cycle.size() < MIN_MODES && caps.supports5g && !cycle.contains(NetworkMode.PREFERRED_5G)) cycle.add(NetworkMode.PREFERRED_5G); + if (cycle.size() < MIN_MODES && !cycle.contains(NetworkMode.FOUR_G_ONLY)) cycle.add(NetworkMode.FOUR_G_ONLY); + } + appPreferences.setTileCycleModes(cycle); + // Cache is purposefully kept alive so UI changes can sync network state + return true; + } + return false; + } + public boolean isValid(List cycle) { if (cycle == null || cycle.size() < MIN_MODES || cycle.size() > MAX_MODES) { return false; diff --git a/app/src/main/java/com/dhangofa/networktoggle/model/DiagnosticError.java b/app/src/main/java/com/dhangofa/networktoggle/model/DiagnosticError.java new file mode 100644 index 0000000..713143d --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/model/DiagnosticError.java @@ -0,0 +1,13 @@ +package com.dhangofa.networktoggle.model; + +public class DiagnosticError { + public final String command; + public final String stderr; + public final long timestamp; + + public DiagnosticError(String command, String stderr, long timestamp) { + this.command = command; + this.stderr = stderr; + this.timestamp = timestamp; + } +} diff --git a/app/src/main/java/com/dhangofa/networktoggle/model/NetworkMode.java b/app/src/main/java/com/dhangofa/networktoggle/model/NetworkMode.java index 4e9b10c..2f0d0ce 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/model/NetworkMode.java +++ b/app/src/main/java/com/dhangofa/networktoggle/model/NetworkMode.java @@ -5,7 +5,9 @@ public enum NetworkMode { 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"); + PREFERRED_4G(4, "Preferred 4G", "Pref 4G", "P4G", "1011111101111111111"), + PREFERRED_3G(5, "Preferred 3G", "Pref 3G", "P3G", "11110101111111111"), + TWO_G_ONLY(6, "2G Only", "2G Only", "2G", "1000000000000011"); private final int stateValue; private final String displayName; @@ -40,6 +42,8 @@ public static NetworkMode nextInDefaultCycle(NetworkMode current) { case FIVE_G_ONLY: return PREFERRED_5G; case PREFERRED_5G: return PREFERRED_4G; case PREFERRED_4G: + case PREFERRED_3G: + case TWO_G_ONLY: case UNKNOWN: default: return FOUR_G_ONLY; } @@ -48,6 +52,21 @@ public static NetworkMode nextInDefaultCycle(NetworkMode current) { public static NetworkMode fromLegacyMode(Integer legacyMode) { if (legacyMode == null) return UNKNOWN; switch (legacyMode) { + case 1: + case 16: + return TWO_G_ONLY; + case 0: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 13: + case 14: + case 18: + case 21: // Global 3G (CDMA+EVDO+GSM+WCDMA) + return PREFERRED_3G; case 11: return FOUR_G_ONLY; case 23: return FIVE_G_ONLY; case 33: return PREFERRED_5G; @@ -59,7 +78,7 @@ public static NetworkMode fromLegacyMode(Integer legacyMode) { case 17: case 19: case 20: - case 22: + case 22: // Global 4G (LTE+CDMA+EVDO+GSM+WCDMA) return PREFERRED_4G; default: if (legacyMode >= 24 && legacyMode <= 32) return PREFERRED_5G; diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/LteAndAboveCarrierRegistry.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/LteAndAboveCarrierRegistry.java new file mode 100644 index 0000000..4b99aea --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/LteAndAboveCarrierRegistry.java @@ -0,0 +1,93 @@ +package com.dhangofa.networktoggle.telephony; + +import java.util.Arrays; +import java.util.List; + +public final class LteAndAboveCarrierRegistry { + + // REGION: INDIA + private static final List INDIA_LTE_AND_ABOVE = Arrays.asList( + "jio", "ind-jio", "jio 4g", "jio true5g" + ); + + // REGION: USA + // Verizon, AT&T, and T-Mobile have fully shut down 2G/3G networks as of 2026. Dish is 5G only. + private static final List USA_LTE_AND_ABOVE = Arrays.asList( + "verizon", "vzw", "at&t", "att", "t-mobile", "tmobile", "dish", "dish wireless" + ); + + // REGION: JAPAN + // Docomo, KDDI (au), SoftBank, and Rakuten have shut down their 3G networks. + private static final List JAPAN_LTE_AND_ABOVE = Arrays.asList( + "docomo", "ntt docomo", "au", "kddi", "softbank", "rakuten", "rakuten mobile" + ); + + // REGION: AUSTRALIA + // Telstra, Optus, and Vodafone Australia (TPG) have fully shut down 3G. + private static final List AUSTRALIA_LTE_AND_ABOVE = Arrays.asList( + "telstra", "optus", "vodafone au", "vodafone australia", "tpg" + ); + + // REGION: TAIWAN + // Chunghwa Telecom, Taiwan Mobile, and Far EasTone fully shut down 3G in mid-2024. + private static final List TAIWAN_LTE_AND_ABOVE = Arrays.asList( + "chunghwa", "chunghwa telecom", "taiwan mobile", "far eastone", "fet" + ); + + // REGION: SINGAPORE + // Singtel, StarHub, and M1 fully shut down 3G in 2024. + private static final List SINGAPORE_LTE_AND_ABOVE = Arrays.asList( + "singtel", "starhub", "m1" + ); + + // REGION: SOUTH KOREA + // LG U+ never had 3G and shut down 2G. (SK Telecom and KT still operate 3G as of 2026). + private static final List SOUTHKOREA_LTE_AND_ABOVE = Arrays.asList( + "lg u+", "lgu+", "lg uplus", "uplus" + ); + + private LteAndAboveCarrierRegistry() { + // Private constructor + } + + /** + * Stage 4: Checks if the carrier name belongs to a known LTE/5G only network. + */ + public static boolean isLteAndAboveOnly(String carrierName) { + if (carrierName == null || carrierName.trim().isEmpty()) { + return false; + } + + String normalized = carrierName.toLowerCase().trim(); + + for (String name : INDIA_LTE_AND_ABOVE) { + if (normalized.equals(name) || normalized.contains(name)) return true; + } + + for (String name : USA_LTE_AND_ABOVE) { + if (normalized.equals(name) || normalized.contains(name)) return true; + } + + for (String name : JAPAN_LTE_AND_ABOVE) { + if (normalized.equals(name) || normalized.contains(name)) return true; + } + + for (String name : AUSTRALIA_LTE_AND_ABOVE) { + if (normalized.equals(name) || normalized.contains(name)) return true; + } + + for (String name : TAIWAN_LTE_AND_ABOVE) { + if (normalized.equals(name) || normalized.contains(name)) return true; + } + + for (String name : SINGAPORE_LTE_AND_ABOVE) { + if (normalized.equals(name) || normalized.contains(name)) return true; + } + + for (String name : SOUTHKOREA_LTE_AND_ABOVE) { + if (normalized.equals(name) || normalized.contains(name)) return true; + } + + return false; + } +} diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkCapabilityResolver.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkCapabilityResolver.java new file mode 100644 index 0000000..c92f1b4 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkCapabilityResolver.java @@ -0,0 +1,157 @@ +package com.dhangofa.networktoggle.telephony; + +import android.os.Build; + +import com.dhangofa.networktoggle.command.CommandExecutor; +import com.dhangofa.networktoggle.command.CommandExecutorFactory; +import com.dhangofa.networktoggle.config.AppPreferences; +import com.dhangofa.networktoggle.config.AppPreferences.NetworkCapabilities; +import com.dhangofa.networktoggle.model.CommandResult; +import com.dhangofa.networktoggle.model.ExecutionMode; + +public final class NetworkCapabilityResolver { + private final AppPreferences appPreferences; + private final SimResolver simResolver; + + public NetworkCapabilityResolver(AppPreferences appPreferences, SimResolver simResolver) { + this.appPreferences = appPreferences; + this.simResolver = simResolver; + } + + public NetworkCapabilities getCapabilities(ExecutionMode mode) { + if (mode == ExecutionMode.NONE) return NetworkCapabilities.assumeAll(); + + // ONE SINGLE CALL to get slotIndex, subId, and carrierName! + SimResolver.SimInfo simInfo = simResolver.resolveTargetSimInfo(mode); + + if (simInfo == null || !simResolver.isValidSlotIndex(simInfo.slotIndex) || !simResolver.isValidSubId(simInfo.subId)) { + return NetworkCapabilities.assumeAll(); + } + + int slotIndex = simInfo.slotIndex; + int subId = simInfo.subId; + String carrierName = simInfo.carrierName; + + // Check Cache + int cachedSubId = appPreferences.getCachedSubIdForSlot(slotIndex); + NetworkCapabilities cachedCaps = appPreferences.getSlotCapabilities(slotIndex); + + if (cachedSubId == subId && cachedCaps != null) { + if (LteAndAboveCarrierRegistry.isLteAndAboveOnly(carrierName)) { + return new NetworkCapabilities(false, false, cachedCaps.supports4g, cachedCaps.supports5g); + } + return cachedCaps; + } + + // Invalidate cache and fetch + appPreferences.invalidateSlotCache(slotIndex); + + // Stage 1 & 2: Global Device/OS Capabilities + NetworkCapabilities deviceCaps = fetchDeviceCapabilities(mode); + + // Stage 3 & 4: Slot-Specific Carrier Capabilities (Pass carrierName directly) + NetworkCapabilities carrierCaps = fetchCarrierCapabilities(mode, slotIndex, carrierName); + + // Combine + NetworkCapabilities finalCaps = new NetworkCapabilities( + deviceCaps.supports2g && carrierCaps.supports2g, + deviceCaps.supports3g && carrierCaps.supports3g, + deviceCaps.supports4g && carrierCaps.supports4g, + deviceCaps.supports5g && carrierCaps.supports5g + ); + + CommandExecutor executor = CommandExecutorFactory.forMode(mode); + if (executor != null) { + CommandResult pingResult = executor.execute("echo test"); + if (pingResult.isSuccess()) { + appPreferences.saveSlotCapabilities(slotIndex, subId, finalCaps); + } + } + return finalCaps; + } + + private NetworkCapabilities fetchDeviceCapabilities(ExecutionMode mode) { + NetworkCapabilities cachedDevice = appPreferences.getDeviceCapabilities(); + if (cachedDevice != null) return cachedDevice; + + boolean supports5g = false; + boolean canCache = true; + + // Stage 1: Android Version Check (Android 11 / API 30+) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + CommandExecutor executor = CommandExecutorFactory.forMode(mode); + if (executor != null) { + // Stage 2: Hardware Ceiling Check + CommandResult result = executor.execute("getprop ro.telephony.default_network"); + if (result.isSuccess()) { + if (!result.getStdout().trim().isEmpty()) { + String[] values = result.getStdout().trim().split(","); + + // Check if ANY value globally supports 5G (>= 23) + for (String val : values) { + Integer parsed = ShellValueParser.extractFirstInt(val); + if (parsed != null && parsed >= 23) { + supports5g = true; + break; + } + } + } + } else { + canCache = false; + } + } else { + canCache = false; + } + } + + NetworkCapabilities deviceCaps = new NetworkCapabilities(true, true, true, supports5g); + if (canCache) { + appPreferences.saveDeviceCapabilities(deviceCaps); + } + return deviceCaps; + } + + private NetworkCapabilities fetchCarrierCapabilities(ExecutionMode mode, int slotIndex, String carrierName) { + CommandExecutor executor = CommandExecutorFactory.forMode(mode); + if (executor == null) return NetworkCapabilities.assumeAll(); + + // Stage 3: Carrier Config XML Verification + String command = "dumpsys carrier_config | grep -E 'Phone Id|hide_enable_2g_bool|carrier_supports_2g_bool|hide_enable_3g_bool|carrier_supports_3g_bool|carrier_nr_availabilities_int_array'"; + CommandResult result = executor.execute(command); + + boolean supports2g = true; + boolean supports3g = true; + boolean supports5g = true; + + if (result.isSuccess() && !result.getStdout().trim().isEmpty()) { + String[] lines = result.getStdout().split("\\n"); + boolean inTargetSlot = false; + + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.startsWith("Phone Id =")) { + Integer currentSlot = ShellValueParser.extractFirstInt(trimmed); + inTargetSlot = (currentSlot != null && currentSlot == slotIndex); + } else if (inTargetSlot) { + if (trimmed.contains("hide_enable_2g_bool = true") || trimmed.contains("carrier_supports_2g_bool = false")) { + supports2g = false; + } + if (trimmed.contains("hide_enable_3g_bool = true") || trimmed.contains("carrier_supports_3g_bool = false")) { + supports3g = false; + } + if (trimmed.startsWith("carrier_nr_availabilities_int_array = []")) { + supports5g = false; + } + } + } + } + + // Stage 4: Smart Blocklist Check + if (LteAndAboveCarrierRegistry.isLteAndAboveOnly(carrierName)) { + supports2g = false; + supports3g = false; + } + + return new NetworkCapabilities(supports2g, supports3g, true, supports5g); + } +} diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java index 68dc0f4..1897832 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/NetworkModeReader.java @@ -7,15 +7,20 @@ import com.dhangofa.networktoggle.model.ExecutionMode; import com.dhangofa.networktoggle.model.NetworkMode; import com.dhangofa.networktoggle.model.TargetSim; +import android.content.Context; +import android.provider.Settings; public final class NetworkModeReader { + private final Context context; private final AppPreferences appPreferences; private final SimResolver simResolver; public NetworkModeReader( + Context context, AppPreferences appPreferences, SimResolver simResolver ) { + this.context = context; this.appPreferences = appPreferences; this.simResolver = simResolver; } @@ -29,6 +34,24 @@ public NetworkMode readCurrentMode() { TargetSim targetSim = appPreferences.getTargetSim(); int targetSubId = simResolver.resolveTargetSubId(executionMode); + // NATIVE API FAST-PATH: + // Try reading natively without spawning shell if we have a valid SubId + if (simResolver.isValidSubId(targetSubId)) { + try { + String nativeValue = Settings.Global.getString( + context.getContentResolver(), + "preferred_network_mode" + targetSubId + ); + + if (nativeValue != null && !nativeValue.trim().isEmpty() && !nativeValue.equalsIgnoreCase("null")) { + return NetworkMode.fromLegacyMode(ShellValueParser.extractFirstInt(nativeValue)); + } + } catch (Exception ignored) { + // Ignore SecurityExceptions or null pointers, proceed to shell fallback + } + } + + // SHELL FALLBACK: String command; if (simResolver.isValidSubId(targetSubId)) { command = "value=$(settings get global preferred_network_mode" @@ -37,6 +60,7 @@ public NetworkMode readCurrentMode() { + "&& [ \"$value\" != \"null\" ]; then " + "echo \"$value\"; else exit 1; fi"; } else if (targetSim == TargetSim.AUTO) { + // Very slow nested shell fallback if native SubId resolution entirely failed command = "data_sim=$(settings get global multi_sim_data_call); " + "[ \"$data_sim\" -gt 0 ] 2>/dev/null || exit 1; " + "settings get global preferred_network_mode${data_sim}"; diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/ShellValueParser.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShellValueParser.java index 078f5af..296a6c9 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/ShellValueParser.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/ShellValueParser.java @@ -3,13 +3,14 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -final class ShellValueParser { - private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+"); +public final class ShellValueParser { + // Updated regex to support negative numbers (e.g., -1 for invalid slot) + private static final Pattern NUMBER_PATTERN = Pattern.compile("-?\\d+"); private ShellValueParser() { } - static Integer extractFirstInt(String text) { + public static Integer extractFirstInt(String text) { try { if (text == null || text.trim().isEmpty() @@ -23,4 +24,35 @@ static Integer extractFirstInt(String text) { return null; } } + + /** + * Finds a specific key (e.g., "simSlotIndex=") and extracts the integer immediately after it. + */ + public static Integer extractIntByKey(String text, String key) { + if (text == null || key == null) return null; + int index = text.indexOf(key); + if (index == -1) return null; + + // Pass only the remainder of the string to find the first int + String remainder = text.substring(index + key.length()); + return extractFirstInt(remainder); + } + + /** + * Extracts a string value located between a starting key and an ending delimiter. + */ + public static String extractStringByKey(String text, String key, String endDelimiter) { + if (text == null || key == null) return ""; + int start = text.indexOf(key); + if (start == -1) return ""; + start += key.length(); + + int end = text.indexOf(endDelimiter, start); + if (end == -1) { + end = text.indexOf(" ", start); // fallback to next space + if (end == -1) end = text.length(); + } + + return text.substring(start, end).trim(); + } } diff --git a/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java b/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java index 3f961ba..ba0dd4e 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java +++ b/app/src/main/java/com/dhangofa/networktoggle/telephony/SimResolver.java @@ -1,6 +1,10 @@ package com.dhangofa.networktoggle.telephony; +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; import android.os.Build; +import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import com.dhangofa.networktoggle.command.CommandExecutor; @@ -14,121 +18,133 @@ public final class SimResolver { public static final int INVALID_SLOT_INDEX = -1; public static final int INVALID_SUB_ID = -1; + private final Context context; private final AppPreferences appPreferences; - public SimResolver(AppPreferences appPreferences) { - this.appPreferences = appPreferences; - } - - public int resolveTargetSlotIndex(ExecutionMode executionMode) { - TargetSim targetSim = appPreferences.getTargetSim(); + // Data class to hold all extracted variables in one place + public static class SimInfo { + public final int subId; + public final int slotIndex; + public final String carrierName; - if (!targetSim.isAuto()) { - appPreferences.setAutoSimError(false); - return targetSim.getManualSlotIndex(); + public SimInfo(int subId, int slotIndex, String carrierName) { + this.subId = subId; + this.slotIndex = slotIndex; + this.carrierName = carrierName; } + } - return resolveAutoSlotIndex(executionMode); + public SimResolver(Context context, AppPreferences appPreferences) { + this.context = context.getApplicationContext(); + this.appPreferences = appPreferences; } - public int resolveTargetSubId(ExecutionMode executionMode) { + /** + * Resolves IDs using Native APIs first. + * Silently falls back to Shell commands if permission is denied or device is too old. + */ + public SimInfo resolveTargetSimInfo(ExecutionMode executionMode) { TargetSim targetSim = appPreferences.getTargetSim(); - - if (targetSim == TargetSim.AUTO) { - int subId = SubscriptionManager.getDefaultDataSubscriptionId(); - return isValidSubId(subId) ? subId : INVALID_SUB_ID; + + int targetSubId = INVALID_SUB_ID; + int targetSlotIndex = INVALID_SLOT_INDEX; + String carrierName = ""; + + // 1. Safe Native APIs (No permission required) + if (targetSim.isAuto()) { + targetSubId = SubscriptionManager.getDefaultDataSubscriptionId(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + int nativeSlot = SubscriptionManager.getSlotIndex(targetSubId); + if (isValidSlotIndex(nativeSlot)) targetSlotIndex = nativeSlot; + } catch (Exception ignored) {} + } + } else { + targetSlotIndex = targetSim.getManualSlotIndex(); } - 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; + // 2. Protected Native APIs (Requires READ_PHONE_STATE) + boolean hasPermission = context.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED; + + if (hasPermission) { + SubscriptionManager sm = context.getSystemService(SubscriptionManager.class); + if (sm != null) { + try { + if (isValidSubId(targetSubId)) { + SubscriptionInfo info = sm.getActiveSubscriptionInfo(targetSubId); + if (info != null) { + if (info.getCarrierName() != null) carrierName = info.getCarrierName().toString(); + if (!isValidSlotIndex(targetSlotIndex)) targetSlotIndex = info.getSimSlotIndex(); + } + } else if (isValidSlotIndex(targetSlotIndex)) { + for (SubscriptionInfo info : sm.getActiveSubscriptionInfoList()) { + if (info.getSimSlotIndex() == targetSlotIndex) { + targetSubId = info.getSubscriptionId(); + if (info.getCarrierName() != null) carrierName = info.getCarrierName().toString(); + break; + } + } + } + } catch (Exception ignored) {} + } } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - try { - int slotIndex = SubscriptionManager.getSlotIndex(dataSubId); - if (isValidSlotIndex(slotIndex)) { - appPreferences.setAutoSimError(false); - return slotIndex; + // 3. Shell Fallback (If missing permission, or older Android OS failed to map) + if (!isValidSubId(targetSubId) || !isValidSlotIndex(targetSlotIndex) || carrierName.isEmpty()) { + String command = "dumpsys isub | grep -E \"\\{id=[0-9]+ .*simSlotIndex=\""; + CommandExecutor executor = CommandExecutorFactory.forMode(executionMode); + + if (executor != null) { + CommandResult result = executor.execute(command); + if (result.isSuccess() && !result.getStdout().trim().isEmpty()) { + String[] lines = result.getStdout().split("\\n"); + for (String line : lines) { + Integer dumpSubId = ShellValueParser.extractIntByKey(line, "id="); + Integer dumpSlotIndex = ShellValueParser.extractIntByKey(line, "simSlotIndex="); + String dumpCarrier = ShellValueParser.extractStringByKey(line, "carrierName=", " nameSource="); + + if (dumpSubId != null && dumpSlotIndex != null) { + if (targetSim.isAuto() && dumpSubId == targetSubId) { + if (!isValidSlotIndex(targetSlotIndex)) targetSlotIndex = dumpSlotIndex; + if (carrierName.isEmpty()) carrierName = dumpCarrier; + break; + } else if (!targetSim.isAuto() && dumpSlotIndex == targetSlotIndex) { + if (!isValidSubId(targetSubId)) targetSubId = dumpSubId; + if (carrierName.isEmpty()) carrierName = dumpCarrier; + break; + } + } + } } - } catch (Exception ignored) { } } - int slotIndex = resolveSlotIndexFromDumpsys(executionMode, dataSubId); - if (isValidSlotIndex(slotIndex)) { - appPreferences.setAutoSimError(false); - return slotIndex; + // 4. Final Validation + if (isValidSubId(targetSubId) && isValidSlotIndex(targetSlotIndex)) { + if (targetSim.isAuto()) appPreferences.setAutoSimError(false); + return new SimInfo(targetSubId, targetSlotIndex, carrierName); } - appPreferences.setAutoSimError(true); - return INVALID_SLOT_INDEX; + if (targetSim.isAuto()) appPreferences.setAutoSimError(true); + return null; + } + + // Keeping backwards compatibility for existing app logic + public int resolveTargetSlotIndex(ExecutionMode executionMode) { + SimInfo info = resolveTargetSimInfo(executionMode); + return info != null ? info.slotIndex : 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; + public int resolveTargetSubId(ExecutionMode executionMode) { + SimInfo info = resolveTargetSimInfo(executionMode); + return info != null ? info.subId : INVALID_SUB_ID; } - 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; + public boolean isValidSlotIndex(int slotIndex) { + return slotIndex == 0 || slotIndex == 1; } - 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); + public boolean isValidSubId(int subId) { + return subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID && subId != INVALID_SUB_ID; } } diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java b/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java index 7b06b9e..f16c70b 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java @@ -1,11 +1,13 @@ package com.dhangofa.networktoggle.ui; import android.app.Activity; +import android.view.View; import android.widget.CheckBox; import android.widget.TextView; import android.widget.Toast; import com.dhangofa.networktoggle.R; +import com.dhangofa.networktoggle.config.AppPreferences; import com.dhangofa.networktoggle.cycle.TileCycleManager; import com.dhangofa.networktoggle.model.NetworkMode; @@ -14,42 +16,154 @@ public final class TileCycleUiController { private final Activity activity; private final TileCycleManager cycleManager; - private final CheckBox mode5gOnly; - private final CheckBox mode4gOnly; + private final CheckBox modePref5g; private final CheckBox modePref4g; + private final CheckBox modePref3g; + private final CheckBox mode5gOnly; + private final CheckBox mode4gOnly; + private final CheckBox mode2gOnly; + + private final View separatorCyclePref1; + private final View separatorCyclePref2; + private final View separatorCycleOnly1; + private final View separatorCycleOnly2; + private final TextView selectedCount; private final TextView cycleOrder; private boolean updatingUi; + private AppPreferences.NetworkCapabilities currentCaps; + private OnCycleChangedListener cycleChangedListener; + private boolean isAuthorized = true; + + public interface OnCycleChangedListener { + void onCycleChanged(List newCycle); + } + public TileCycleUiController(Activity activity, TileCycleManager cycleManager) { this.activity = activity; this.cycleManager = cycleManager; - mode5gOnly = activity.findViewById(R.id.cycle5gOnly); - mode4gOnly = activity.findViewById(R.id.cycle4gOnly); + modePref5g = activity.findViewById(R.id.cyclePreferred5g); modePref4g = activity.findViewById(R.id.cyclePreferred4g); + modePref3g = activity.findViewById(R.id.cyclePreferred3g); + mode5gOnly = activity.findViewById(R.id.cycle5gOnly); + mode4gOnly = activity.findViewById(R.id.cycle4gOnly); + mode2gOnly = activity.findViewById(R.id.cycle2gOnly); + + separatorCyclePref1 = activity.findViewById(R.id.separatorCyclePref1); + separatorCyclePref2 = activity.findViewById(R.id.separatorCyclePref2); + separatorCycleOnly1 = activity.findViewById(R.id.separatorCycleOnly1); + separatorCycleOnly2 = activity.findViewById(R.id.separatorCycleOnly2); + selectedCount = activity.findViewById(R.id.cycleSelectedCount); cycleOrder = activity.findViewById(R.id.cycleOrderText); } + public void setOnCycleChangedListener(OnCycleChangedListener listener) { + this.cycleChangedListener = listener; + } + public void initialize() { refresh(); - mode5gOnly.setOnCheckedChangeListener((button, selected) -> - handleSelection(NetworkMode.FIVE_G_ONLY, selected)); - mode4gOnly.setOnCheckedChangeListener((button, selected) -> - handleSelection(NetworkMode.FOUR_G_ONLY, selected)); + + android.view.View.OnTouchListener lockTouch = (v, event) -> { + if (!isAuthorized && event.getAction() == android.view.MotionEvent.ACTION_DOWN) { + showToast("Please authorize Root or Shizuku to configure toggles."); + return true; // Consume event to prevent visual change + } + return false; + }; + modePref5g.setOnTouchListener(lockTouch); + modePref4g.setOnTouchListener(lockTouch); + modePref3g.setOnTouchListener(lockTouch); + mode5gOnly.setOnTouchListener(lockTouch); + mode4gOnly.setOnTouchListener(lockTouch); + mode2gOnly.setOnTouchListener(lockTouch); + modePref5g.setOnCheckedChangeListener((button, selected) -> handleSelection(NetworkMode.PREFERRED_5G, selected)); modePref4g.setOnCheckedChangeListener((button, selected) -> handleSelection(NetworkMode.PREFERRED_4G, selected)); + modePref3g.setOnCheckedChangeListener((button, selected) -> + handleSelection(NetworkMode.PREFERRED_3G, selected)); + mode5gOnly.setOnCheckedChangeListener((button, selected) -> + handleSelection(NetworkMode.FIVE_G_ONLY, selected)); + mode4gOnly.setOnCheckedChangeListener((button, selected) -> + handleSelection(NetworkMode.FOUR_G_ONLY, selected)); + mode2gOnly.setOnCheckedChangeListener((button, selected) -> + handleSelection(NetworkMode.TWO_G_ONLY, selected)); + } + + public void setAuthorized(boolean authorized) { + if (this.isAuthorized == authorized) return; + this.isAuthorized = authorized; + float alpha = authorized ? 1.0f : 0.4f; + + View card = activity.findViewById(R.id.cardTileCycle); + if (card != null) { + card.setAlpha(alpha); + } + } + + public void applyCapabilities(AppPreferences.NetworkCapabilities caps) { + if (caps == null) return; + this.currentCaps = caps; + + modePref5g.setAlpha(caps.supports5g ? 1.0f : 0.4f); + mode5gOnly.setAlpha(caps.supports5g ? 1.0f : 0.4f); + + modePref3g.setAlpha(caps.supports3g ? 1.0f : 0.4f); + + mode2gOnly.setAlpha(caps.supports2g ? 1.0f : 0.4f); + + if (cycleManager.forceRemoveUnsupportedAndAutoFill(caps)) { + showToast("Cycle auto-adjusted for current SIM capabilities"); + if (cycleChangedListener != null) { + cycleChangedListener.onCycleChanged(cycleManager.getCycle()); + } + } + + refresh(); } private void handleSelection(NetworkMode mode, boolean selected) { if (updatingUi) return; + if (!isAuthorized) { + showToast("Please authorize Root or Shizuku to configure toggles."); + refresh(); // Revert checkbox visual change + return; + } + + if (selected && currentCaps != null) { + boolean supported = true; + String reason = ""; + if ((mode == NetworkMode.PREFERRED_5G || mode == NetworkMode.FIVE_G_ONLY) && !currentCaps.supports5g) { + supported = false; + reason = "5G is not supported by your device or current carrier."; + } else if (mode == NetworkMode.PREFERRED_3G && !currentCaps.supports3g) { + supported = false; + reason = "3G is disabled or not supported by your current carrier."; + } else if (mode == NetworkMode.TWO_G_ONLY && !currentCaps.supports2g) { + supported = false; + reason = "2G is disabled or not supported by your current carrier."; + } + + if (!supported) { + showToast(reason); + refresh(); // Revert UI to match the actual saved cycle + return; + } + } + TileCycleManager.ChangeResult result = cycleManager.setSelected(mode, selected); - if (result == TileCycleManager.ChangeResult.MINIMUM_REACHED) { + if (result == TileCycleManager.ChangeResult.CHANGED) { + if (cycleChangedListener != null) { + cycleChangedListener.onCycleChanged(cycleManager.getCycle()); + } + } else if (result == TileCycleManager.ChangeResult.MINIMUM_REACHED) { showToast("Select at least 2 tile modes."); } else if (result == TileCycleManager.ChangeResult.MAXIMUM_REACHED) { showToast("You can select up to 3 tile modes."); @@ -60,10 +174,20 @@ private void handleSelection(NetworkMode mode, boolean selected) { private void refresh() { updatingUi = true; List cycle = cycleManager.getCycle(); - mode5gOnly.setChecked(cycle.contains(NetworkMode.FIVE_G_ONLY)); - mode4gOnly.setChecked(cycle.contains(NetworkMode.FOUR_G_ONLY)); + modePref5g.setChecked(cycle.contains(NetworkMode.PREFERRED_5G)); modePref4g.setChecked(cycle.contains(NetworkMode.PREFERRED_4G)); + modePref3g.setChecked(cycle.contains(NetworkMode.PREFERRED_3G)); + mode5gOnly.setChecked(cycle.contains(NetworkMode.FIVE_G_ONLY)); + mode4gOnly.setChecked(cycle.contains(NetworkMode.FOUR_G_ONLY)); + mode2gOnly.setChecked(cycle.contains(NetworkMode.TWO_G_ONLY)); + + // Hide separators if either adjacent button is checked to create a seamless pill background + separatorCyclePref1.setVisibility(modePref5g.isChecked() || modePref4g.isChecked() ? View.INVISIBLE : View.VISIBLE); + separatorCyclePref2.setVisibility(modePref4g.isChecked() || modePref3g.isChecked() ? View.INVISIBLE : View.VISIBLE); + separatorCycleOnly1.setVisibility(mode5gOnly.isChecked() || mode4gOnly.isChecked() ? View.INVISIBLE : View.VISIBLE); + separatorCycleOnly2.setVisibility(mode4gOnly.isChecked() || mode2gOnly.isChecked() ? View.INVISIBLE : View.VISIBLE); + selectedCount.setText("Selected: " + cycle.size() + "/3"); cycleOrder.setText(buildOrderText(cycle)); updatingUi = false; @@ -82,4 +206,3 @@ private void showToast(String message) { Toast.makeText(activity, message, Toast.LENGTH_SHORT).show(); } } - diff --git a/app/src/main/java/com/dhangofa/networktoggle/util/DiagnosticReporter.java b/app/src/main/java/com/dhangofa/networktoggle/util/DiagnosticReporter.java new file mode 100644 index 0000000..c70c9e0 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/util/DiagnosticReporter.java @@ -0,0 +1,48 @@ +package com.dhangofa.networktoggle.util; + +import android.os.Build; +import com.dhangofa.networktoggle.config.AppPreferences; +import com.dhangofa.networktoggle.model.DiagnosticError; +import com.dhangofa.networktoggle.telephony.SimResolver; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class DiagnosticReporter { + + public static String generateReport(AppPreferences prefs, SimResolver simResolver) { + DiagnosticError error = prefs.getLastError(); + if (error == null) { + return "No recent errors recorded."; + } + + StringBuilder sb = new StringBuilder(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); + + sb.append("--- NETWORK TOGGLE DIAGNOSTIC REPORT ---\n"); + sb.append("Time: ").append(sdf.format(new Date(error.timestamp))).append("\n\n"); + + sb.append("[DEVICE INFO]\n"); + sb.append("Manufacturer: ").append(Build.MANUFACTURER).append("\n"); + sb.append("Brand: ").append(Build.BRAND).append("\n"); + sb.append("Device: ").append(Build.DEVICE).append("\n"); + sb.append("Model: ").append(Build.MODEL).append("\n"); + sb.append("Android Version: ").append(Build.VERSION.RELEASE).append("\n"); + sb.append("SDK Level: ").append(Build.VERSION.SDK_INT).append("\n\n"); + + sb.append("[APP STATE]\n"); + sb.append("Execution Mode: ").append(prefs.getExecutionMode().name()).append("\n"); + sb.append("Target SIM Setting: ").append(prefs.getTargetSim().name()).append("\n"); + + int slotIndex = simResolver.resolveTargetSlotIndex(prefs.getExecutionMode()); + sb.append("Resolved Slot Index: ").append(slotIndex).append("\n\n"); + + sb.append("[ERROR DETAILS]\n"); + sb.append("Command Attempted:\n").append(error.command).append("\n\n"); + sb.append("Standard Error (stderr):\n").append(error.stderr).append("\n\n"); + + sb.append("--- END OF REPORT ---"); + + return sb.toString(); + } +} diff --git a/app/src/main/res/color/color_segmented_text_cycle.xml b/app/src/main/res/color/color_segmented_text_cycle.xml new file mode 100644 index 0000000..d9152f6 --- /dev/null +++ b/app/src/main/res/color/color_segmented_text_cycle.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/color/color_segmented_text_exec.xml b/app/src/main/res/color/color_segmented_text_exec.xml new file mode 100644 index 0000000..49c7c53 --- /dev/null +++ b/app/src/main/res/color/color_segmented_text_exec.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_right.xml b/app/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 0000000..173ef1f --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..852af25 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_speed.xml b/app/src/main/res/drawable/ic_speed.xml new file mode 100644 index 0000000..1c926a1 --- /dev/null +++ b/app/src/main/res/drawable/ic_speed.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_warning.xml b/app/src/main/res/drawable/ic_warning.xml new file mode 100644 index 0000000..269f9a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_warning.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/section_outline_bg.xml b/app/src/main/res/drawable/section_outline_bg.xml index 18966ae..98e1e46 100644 --- a/app/src/main/res/drawable/section_outline_bg.xml +++ b/app/src/main/res/drawable/section_outline_bg.xml @@ -2,13 +2,10 @@ - - + + android:left="16dp" + android:top="16dp" + android:right="16dp" + android:bottom="16dp" /> diff --git a/app/src/main/res/drawable/selector_segmented_tab.xml b/app/src/main/res/drawable/selector_segmented_tab.xml index 3377f2c..5dfadb5 100644 --- a/app/src/main/res/drawable/selector_segmented_tab.xml +++ b/app/src/main/res/drawable/selector_segmented_tab.xml @@ -3,14 +3,14 @@ - + - + diff --git a/app/src/main/res/drawable/selector_segmented_tab_cycle.xml b/app/src/main/res/drawable/selector_segmented_tab_cycle.xml new file mode 100644 index 0000000..89b5b3f --- /dev/null +++ b/app/src/main/res/drawable/selector_segmented_tab_cycle.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/selector_segmented_tab_exec.xml b/app/src/main/res/drawable/selector_segmented_tab_exec.xml new file mode 100644 index 0000000..28f41d0 --- /dev/null +++ b/app/src/main/res/drawable/selector_segmented_tab_exec.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/selector_segmented_tab_rect.xml b/app/src/main/res/drawable/selector_segmented_tab_rect.xml new file mode 100644 index 0000000..66d1e06 --- /dev/null +++ b/app/src/main/res/drawable/selector_segmented_tab_rect.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/shape_app_icon_bg.xml b/app/src/main/res/drawable/shape_app_icon_bg.xml new file mode 100644 index 0000000..6db87ae --- /dev/null +++ b/app/src/main/res/drawable/shape_app_icon_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/shape_button_primary.xml b/app/src/main/res/drawable/shape_button_primary.xml new file mode 100644 index 0000000..efa2239 --- /dev/null +++ b/app/src/main/res/drawable/shape_button_primary.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/shape_cycle_card_bg.xml b/app/src/main/res/drawable/shape_cycle_card_bg.xml new file mode 100644 index 0000000..b2d7950 --- /dev/null +++ b/app/src/main/res/drawable/shape_cycle_card_bg.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/shape_radio_item_bg.xml b/app/src/main/res/drawable/shape_cycle_order_badge.xml similarity index 59% rename from app/src/main/res/drawable/shape_radio_item_bg.xml rename to app/src/main/res/drawable/shape_cycle_order_badge.xml index e57ef6a..e8ebe00 100644 --- a/app/src/main/res/drawable/shape_radio_item_bg.xml +++ b/app/src/main/res/drawable/shape_cycle_order_badge.xml @@ -1,6 +1,6 @@ - - + + diff --git a/app/src/main/res/drawable/shape_dialog_bg.xml b/app/src/main/res/drawable/shape_dialog_bg.xml new file mode 100644 index 0000000..98d1f63 --- /dev/null +++ b/app/src/main/res/drawable/shape_dialog_bg.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/drawable/shape_exec_card_bg.xml b/app/src/main/res/drawable/shape_exec_card_bg.xml new file mode 100644 index 0000000..27e9033 --- /dev/null +++ b/app/src/main/res/drawable/shape_exec_card_bg.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/shape_floating_footer.xml b/app/src/main/res/drawable/shape_floating_footer.xml new file mode 100644 index 0000000..f420756 --- /dev/null +++ b/app/src/main/res/drawable/shape_floating_footer.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/shape_qs_hint_bg.xml b/app/src/main/res/drawable/shape_qs_hint_bg.xml index e7140d2..8f2c988 100644 --- a/app/src/main/res/drawable/shape_qs_hint_bg.xml +++ b/app/src/main/res/drawable/shape_qs_hint_bg.xml @@ -2,8 +2,5 @@ - - + diff --git a/app/src/main/res/drawable/shape_segmented_container.xml b/app/src/main/res/drawable/shape_segmented_container.xml index 93b7f91..3537330 100644 --- a/app/src/main/res/drawable/shape_segmented_container.xml +++ b/app/src/main/res/drawable/shape_segmented_container.xml @@ -2,6 +2,6 @@ - + diff --git a/app/src/main/res/drawable/shape_segmented_container_rect.xml b/app/src/main/res/drawable/shape_segmented_container_rect.xml new file mode 100644 index 0000000..9047d42 --- /dev/null +++ b/app/src/main/res/drawable/shape_segmented_container_rect.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/shape_small_divider.xml b/app/src/main/res/drawable/shape_small_divider.xml new file mode 100644 index 0000000..9ee6d7c --- /dev/null +++ b/app/src/main/res/drawable/shape_small_divider.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/social_icon_bg.xml b/app/src/main/res/drawable/social_icon_bg.xml index 0bd8189..fd5528c 100644 --- a/app/src/main/res/drawable/social_icon_bg.xml +++ b/app/src/main/res/drawable/social_icon_bg.xml @@ -1,8 +1,6 @@ + android:shape="rectangle"> - + diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml new file mode 100644 index 0000000..34f4c44 --- /dev/null +++ b/app/src/main/res/layout-land/activity_main.xml @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ad77aef..9784d05 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,405 +1,262 @@ - + android:orientation="vertical" + android:background="@color/surface_background"> - + + android:paddingTop="12dp" + android:paddingBottom="12dp" + android:elevation="6dp" + android:outlineProvider="bounds" + android:gravity="center_vertical"> - - + + + + + - + + - - + + + - - + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:orientation="horizontal" + android:background="@drawable/shape_qs_hint_bg" + android:padding="12dp" + android:gravity="center_vertical" + android:layout_marginTop="0dp" + android:layout_marginBottom="12dp"> + + + + + + + + + + + + + + + + - - + - - + android:layout_marginBottom="12dp" /> + + + + + + - + + + + - - + - - - - - - - - - - - - - - + - - + + - + - + - - - - - + + + + diff --git a/app/src/main/res/layout/bottom_sheet_permission.xml b/app/src/main/res/layout/bottom_sheet_permission.xml new file mode 100644 index 0000000..4f473b8 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_permission.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + +