= _configSaved.asStateFlow()
+
+ init {
+ loadCurrentConfiguration()
+ }
+
+ private fun loadCurrentConfiguration() {
+ viewModelScope.launch {
+ try {
+ val config = getToggleModeConfigUseCase()
+ _currentConfig.value = config
+ } catch (e: Exception) {
+ // Use default configuration if loading fails
+ _currentConfig.value = ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY)
+ }
+ }
+ }
+
+ fun updateModeA(mode: NetworkMode) {
+ _currentConfig.value = _currentConfig.value.copy(modeA = mode)
+ }
+
+ fun updateModeB(mode: NetworkMode) {
+ _currentConfig.value = _currentConfig.value.copy(modeB = mode)
+ }
+
+ fun saveConfiguration() {
+ if (_currentConfig.value.modeA == _currentConfig.value.modeB) {
+ return // Don't save if both modes are the same
+ }
+
+ _isLoading.value = true
+ viewModelScope.launch {
+ try {
+ updateToggleModeConfigUseCase(_currentConfig.value)
+ _configSaved.value = true
+ } catch (e: Exception) {
+ // Handle error (could show toast or snackbar)
+ } finally {
+ _isLoading.value = false
+ }
+ }
+ }
+
+ fun resetSavedState() {
+ _configSaved.value = false
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/supernova/networkswitch/service/NetworkTileService.kt b/app/src/main/java/com/supernova/networkswitch/service/NetworkTileService.kt
index 471d231..0fa943d 100644
--- a/app/src/main/java/com/supernova/networkswitch/service/NetworkTileService.kt
+++ b/app/src/main/java/com/supernova/networkswitch/service/NetworkTileService.kt
@@ -4,8 +4,11 @@ import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import android.telephony.SubscriptionManager
import com.supernova.networkswitch.domain.model.ControlMethod
-import com.supernova.networkswitch.domain.usecase.GetNetworkStateUseCase
+import com.supernova.networkswitch.domain.model.NetworkMode
+import com.supernova.networkswitch.domain.model.ToggleModeConfig
+import com.supernova.networkswitch.domain.usecase.GetCurrentNetworkModeUseCase
import com.supernova.networkswitch.domain.usecase.ToggleNetworkModeUseCase
+import com.supernova.networkswitch.domain.usecase.GetToggleModeConfigUseCase
import com.supernova.networkswitch.domain.repository.PreferencesRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
@@ -15,28 +18,33 @@ import javax.inject.Inject
class NetworkTileService : TileService() {
@Inject
- lateinit var getNetworkStateUseCase: GetNetworkStateUseCase
+ lateinit var getCurrentNetworkModeUseCase: GetCurrentNetworkModeUseCase
@Inject
lateinit var toggleNetworkModeUseCase: ToggleNetworkModeUseCase
+ @Inject
+ lateinit var getToggleModeConfigUseCase: GetToggleModeConfigUseCase
+
@Inject
lateinit var preferencesRepository: PreferencesRepository
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
- private var preferredMethod = ControlMethod.SHIZUKU
- private var currentNetworkState = false
+ private var currentNetworkMode: NetworkMode? = null
+ private var toggleConfig: ToggleModeConfig? = null
override fun onStartListening() {
super.onStartListening()
- // Load preferred method and initial state
serviceScope.launch {
try {
- preferredMethod = preferencesRepository.getControlMethod()
- refreshNetworkState()
- } catch (e: Exception) {
- // Silently handle errors during initialization
+ // Observe toggle configuration changes
+ preferencesRepository.observeToggleModeConfig().collect { newConfig ->
+ toggleConfig = newConfig
+ refreshNetworkState()
+ }
+ } catch (_: Exception) {
+ // Handle errors silently
}
}
}
@@ -53,70 +61,65 @@ class NetworkTileService : TileService() {
serviceScope.launch {
try {
- // Update preferred method from settings in case it changed
- preferredMethod = preferencesRepository.getControlMethod()
-
- // Toggle network mode using use case
toggleNetworkModeUseCase(subId)
- .onSuccess {
- // Refresh state to show current status for user feedback
- refreshNetworkState()
+ .onSuccess { newMode ->
+ currentNetworkMode = newMode
+ withContext(Dispatchers.Main) {
+ updateTile()
+ }
}
.onFailure {
- // On failure, still try to refresh state
refreshNetworkState()
}
- } catch (e: Exception) {
- // Silently handle any errors during toggle operation
- try {
- refreshNetworkState()
- } catch (refreshException: Exception) {
- // Silently handle refresh errors too
- }
+ } catch (_: Exception) {
+ refreshNetworkState()
}
}
}
- private fun refreshNetworkState() {
+ private suspend fun refreshNetworkState() {
val subId = SubscriptionManager.getDefaultDataSubscriptionId()
- serviceScope.launch {
- try {
- getNetworkStateUseCase(subId)
- .onSuccess { networkState ->
- currentNetworkState = networkState
- withContext(Dispatchers.Main) {
- updateTile(currentNetworkState)
- }
- }
- .onFailure {
- // Silently handle any errors during state refresh
+ try {
+ getCurrentNetworkModeUseCase(subId)
+ .onSuccess { networkMode ->
+ currentNetworkMode = networkMode
+ withContext(Dispatchers.Main) {
+ updateTile()
}
- } catch (e: Exception) {
- // Silently handle any errors during state refresh
- }
+ }
+ } catch (_: Exception) {
+ // Handle errors silently
}
}
- private fun updateTile(is5gEnabled: Boolean) {
+ private fun updateTile() {
try {
qsTile?.apply {
- state = if (is5gEnabled) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
- label = if (is5gEnabled) "5G Mode" else "4G Mode"
- subtitle = if (is5gEnabled) "NR Only" else "LTE Only"
+ val config = toggleConfig
+
+ if (config != null) {
+ state = Tile.STATE_ACTIVE
+
+ // Show current and next modes
+ val currentMode = config.getCurrentMode()
+ val nextMode = config.getNextMode()
+ label = currentMode.displayName
+ subtitle = "${nextMode.displayName}"
+ } else {
+ state = Tile.STATE_INACTIVE
+ label = "Network Mode"
+ subtitle = "Config not loaded"
+ }
updateTile()
}
- } catch (e: Exception) {
- // Silently handle any tile update errors
+ } catch (_: Exception) {
+ // Handle tile update errors silently
}
}
override fun onDestroy() {
super.onDestroy()
- try {
- serviceScope.cancel()
- } catch (e: Exception) {
- // Silently handle any cleanup errors
- }
+ serviceScope.cancel()
}
}
diff --git a/app/src/main/java/com/supernova/networkswitch/service/RootNetworkControllerService.kt b/app/src/main/java/com/supernova/networkswitch/service/RootNetworkControllerService.kt
index 7308679..a582be3 100644
--- a/app/src/main/java/com/supernova/networkswitch/service/RootNetworkControllerService.kt
+++ b/app/src/main/java/com/supernova/networkswitch/service/RootNetworkControllerService.kt
@@ -1,6 +1,5 @@
package com.supernova.networkswitch.service
-import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
@@ -9,10 +8,6 @@ import com.android.internal.telephony.ITelephony
import com.supernova.networkswitch.IRootController
import com.topjohnwu.superuser.ipc.RootService
-/**
- * Modern root-based network controller service
- * Implements pure network mode switching with clean architecture principles
- */
class RootNetworkControllerService : RootService() {
companion object {
@@ -25,47 +20,118 @@ class RootNetworkControllerService : RootService() {
.getDeclaredField("ALLOWED_NETWORK_TYPES_REASON_USER")
.getInt(null)
}
-
- @delegate:SuppressLint("PrivateApi", "BlockedPrivateApi")
- private val modeLteOnly: Int by lazy {
- try {
- Class.forName("com.android.internal.telephony.RILConstants")
- .getDeclaredField("NETWORK_MODE_LTE_ONLY")
- .getInt(null)
+
+ private fun getBitmask(fieldName: String): Long {
+ return try {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField(fieldName)
+ .getLong(null)
} catch (e: Exception) {
- 11 // Fallback: NETWORK_MODE_LTE_GSM_WCDMA
+ 0L
}
}
-
- @delegate:SuppressLint("PrivateApi", "BlockedPrivateApi")
- private val modeNrOnly: Int by lazy {
- try {
- Class.forName("com.android.internal.telephony.RILConstants")
- .getDeclaredField("NETWORK_MODE_NR_ONLY")
- .getInt(null)
- } catch (e: Exception) {
- 23 // Fallback: NETWORK_MODE_NR_LTE
- }
+
+ private val bitmasks = mapOf(
+ "GSM" to lazy { getBitmask("NETWORK_TYPE_BITMASK_GSM") },
+ "GPRS" to lazy { getBitmask("NETWORK_TYPE_BITMASK_GPRS") },
+ "EDGE" to lazy { getBitmask("NETWORK_TYPE_BITMASK_EDGE") },
+ "UMTS" to lazy { getBitmask("NETWORK_TYPE_BITMASK_UMTS") },
+ "CDMA" to lazy { getBitmask("NETWORK_TYPE_BITMASK_CDMA") },
+ "EVDO_0" to lazy { getBitmask("NETWORK_TYPE_BITMASK_EVDO_0") },
+ "EVDO_A" to lazy { getBitmask("NETWORK_TYPE_BITMASK_EVDO_A") },
+ "1xRTT" to lazy { getBitmask("NETWORK_TYPE_BITMASK_1xRTT") },
+ "HSDPA" to lazy { getBitmask("NETWORK_TYPE_BITMASK_HSDPA") },
+ "HSUPA" to lazy { getBitmask("NETWORK_TYPE_BITMASK_HSUPA") },
+ "HSPA" to lazy { getBitmask("NETWORK_TYPE_BITMASK_HSPA") },
+ "EVDO_B" to lazy { getBitmask("NETWORK_TYPE_BITMASK_EVDO_B") },
+ "LTE" to lazy { getBitmask("NETWORK_TYPE_BITMASK_LTE") },
+ "EHRPD" to lazy { getBitmask("NETWORK_TYPE_BITMASK_EHRPD") },
+ "HSPAP" to lazy { getBitmask("NETWORK_TYPE_BITMASK_HSPAP") },
+ "TD_SCDMA" to lazy { getBitmask("NETWORK_TYPE_BITMASK_TD_SCDMA") },
+ "LTE_CA" to lazy { getBitmask("NETWORK_TYPE_BITMASK_LTE_CA") },
+ "IWLAN" to lazy { getBitmask("NETWORK_TYPE_BITMASK_IWLAN") },
+ "NR" to lazy { getBitmask("NETWORK_TYPE_BITMASK_NR") }
+ )
+
+ private fun getMask(key: String) = bitmasks[key]?.value ?: 0L
+
+ private fun get2GBitmask(): Long {
+ return getMask("GSM") or getMask("GPRS") or getMask("EDGE") or getMask("CDMA") or getMask("1xRTT")
}
- // Android 12+ bitmasks for pure modes
- private val typeLteOnly: Long by lazy {
- try {
- Class.forName("android.telephony.TelephonyManager")
- .getDeclaredField("NETWORK_TYPE_BITMASK_LTE")
- .getLong(null)
- } catch (e: Exception) {
- 524288L // Common LTE bitmask value
+ private fun get3GBitmask(): Long {
+ return getMask("EVDO_0") or getMask("EVDO_A") or getMask("EVDO_B") or getMask("EHRPD") or
+ getMask("HSUPA") or getMask("HSDPA") or getMask("HSPA") or getMask("HSPAP") or
+ getMask("UMTS") or getMask("TD_SCDMA")
+ }
+
+ private fun get4GBitmask(): Long {
+ return getMask("LTE") or getMask("LTE_CA") or getMask("IWLAN")
+ }
+
+ private fun get5GBitmask(): Long {
+ return getMask("NR")
+ }
+
+ private fun mapNetworkModeToBitmask(networkMode: Int): Long {
+ return when (networkMode) {
+ 0 -> get2GBitmask() or get3GBitmask()
+ 1 -> getMask("GSM")
+ 2 -> getMask("UMTS")
+ 3 -> get2GBitmask() or get3GBitmask()
+ 4 -> getMask("CDMA") or getMask("EVDO_0") or getMask("EVDO_A") or getMask("EVDO_B")
+ 5 -> getMask("CDMA")
+ 6 -> getMask("EVDO_0") or getMask("EVDO_A") or getMask("EVDO_B")
+ 7 -> get2GBitmask() or get3GBitmask() or get4GBitmask()
+ 8 -> getMask("LTE") or getMask("CDMA") or getMask("EVDO_0") or getMask("EVDO_A") or getMask("EVDO_B")
+ 9 -> getMask("LTE") or get2GBitmask() or get3GBitmask()
+ 10 -> getMask("LTE") or getMask("CDMA") or getMask("EVDO_0") or getMask("EVDO_A") or getMask("EVDO_B") or get2GBitmask() or get3GBitmask()
+ 11 -> getMask("LTE")
+ 12 -> getMask("LTE") or get3GBitmask()
+ 13 -> getMask("TD_SCDMA")
+ 14 -> getMask("TD_SCDMA") or getMask("UMTS")
+ 15 -> getMask("LTE") or getMask("TD_SCDMA")
+ 16 -> getMask("TD_SCDMA") or get2GBitmask()
+ 17 -> getMask("LTE") or getMask("TD_SCDMA") or get2GBitmask()
+ 18 -> getMask("TD_SCDMA") or get2GBitmask() or get3GBitmask()
+ 19 -> getMask("LTE") or getMask("TD_SCDMA") or get3GBitmask()
+ 20 -> getMask("LTE") or getMask("TD_SCDMA") or get2GBitmask() or get3GBitmask()
+ 21 -> getMask("TD_SCDMA") or getMask("CDMA") or getMask("EVDO_0") or getMask("EVDO_A") or getMask("EVDO_B") or get2GBitmask() or get3GBitmask()
+ 22 -> getMask("LTE") or getMask("TD_SCDMA") or getMask("CDMA") or getMask("EVDO_0") or getMask("EVDO_A") or getMask("EVDO_B") or get2GBitmask() or get3GBitmask()
+ 23 -> getMask("NR")
+ 24 -> getMask("NR") or getMask("LTE")
+ 25 -> getMask("NR") or getMask("LTE") or getMask("CDMA") or getMask("EVDO_0") or getMask("EVDO_A") or getMask("EVDO_B")
+ 26 -> getMask("NR") or getMask("LTE") or get2GBitmask() or get3GBitmask()
+ 27 -> getMask("NR") or getMask("LTE") or getMask("CDMA") or getMask("EVDO_0") or getMask("EVDO_A") or getMask("EVDO_B") or get2GBitmask() or get3GBitmask()
+ 28 -> getMask("NR") or getMask("LTE") or get3GBitmask()
+ 29 -> getMask("NR") or getMask("LTE") or getMask("TD_SCDMA")
+ 30 -> getMask("NR") or getMask("LTE") or getMask("TD_SCDMA") or get2GBitmask()
+ 31 -> getMask("NR") or getMask("LTE") or getMask("TD_SCDMA") or get3GBitmask()
+ 32 -> getMask("NR") or getMask("LTE") or getMask("TD_SCDMA") or get2GBitmask() or get3GBitmask()
+ 33 -> getMask("NR") or getMask("LTE") or getMask("TD_SCDMA") or getMask("CDMA") or getMask("EVDO_0") or getMask("EVDO_A") or getMask("EVDO_B") or get2GBitmask() or get3GBitmask()
+ else -> get2GBitmask() or get3GBitmask() or get4GBitmask()
}
}
- private val typeNrOnly: Long by lazy {
- try {
- Class.forName("android.telephony.TelephonyManager")
- .getDeclaredField("NETWORK_TYPE_BITMASK_NR")
- .getLong(null)
- } catch (e: Exception) {
- 2097152L // Common NR-only bitmask value
+ private fun mapBitmaskToNetworkMode(bitmask: Long): Int {
+ for (mode in 0..33) {
+ if (bitmask == mapNetworkModeToBitmask(mode)) {
+ return mode
+ }
+ }
+
+ return when {
+ bitmask == getMask("NR") -> 23
+ bitmask == getMask("LTE") -> 11
+ bitmask == getMask("GSM") -> 1
+ bitmask == getMask("UMTS") -> 2
+ bitmask == getMask("TD_SCDMA") -> 13
+ bitmask == (getMask("NR") or getMask("LTE")) -> 24
+ (bitmask and getMask("NR")) != 0L -> 23
+ (bitmask and getMask("LTE")) != 0L -> 11
+ (bitmask and get3GBitmask()) != 0L -> 2
+ (bitmask and get2GBitmask()) != 0L -> 1
+ else -> 0
}
}
}
@@ -75,75 +141,47 @@ class RootNetworkControllerService : RootService() {
override fun compatibilityCheck(subId: Int): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- // Test if we can read/write allowed network types
reasonUser
- typeLteOnly
- typeNrOnly
-
- val originalTypes = iTelephony.getAllowedNetworkTypesForReason(subId, reasonUser)
- iTelephony.setAllowedNetworkTypesForReason(subId, reasonUser, originalTypes)
- true
+ iTelephony.setAllowedNetworkTypesForReason(
+ subId,
+ reasonUser,
+ iTelephony.getAllowedNetworkTypesForReason(subId, reasonUser)
+ )
} else {
- // Test preferred network mode switching for Android 11 and below
- modeLteOnly
- modeNrOnly
- val original = iTelephony.getPreferredNetworkType(subId)
- iTelephony.setPreferredNetworkType(subId, original)
- true
+ iTelephony.setPreferredNetworkType(
+ subId,
+ iTelephony.getPreferredNetworkType(subId)
+ )
}
- } catch (e: Exception) {
+ true
+ } catch (_: Exception) {
false
}
}
- override fun getNetworkState(subId: Int): Boolean {
+ override fun getCurrentNetworkMode(subId: Int): Int {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- val currentTypes = iTelephony.getAllowedNetworkTypesForReason(subId, reasonUser)
- // Check if NR (5G) is enabled
- (currentTypes and typeNrOnly) != 0L
+ val currentBitmask = iTelephony.getAllowedNetworkTypesForReason(subId, reasonUser)
+ mapBitmaskToNetworkMode(currentBitmask)
} else {
- val currentMode = iTelephony.getPreferredNetworkType(subId)
- // Check if current mode is a 5G mode
- currentMode == modeNrOnly || currentMode >= 23
+ iTelephony.getPreferredNetworkType(subId)
}
- } catch (e: Exception) {
- false
+ } catch (_: Exception) {
+ -1
}
}
- override fun setNetworkState(subId: Int, enabled: Boolean) {
+ override fun setNetworkMode(subId: Int, networkMode: Int) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- if (enabled) {
- // Set pure 5G mode (NR only)
- iTelephony.setAllowedNetworkTypesForReason(subId, reasonUser, typeNrOnly)
- } else {
- // Set pure 4G mode (LTE only)
- iTelephony.setAllowedNetworkTypesForReason(subId, reasonUser, typeLteOnly)
- }
+ val networkTypeBitmask = mapNetworkModeToBitmask(networkMode)
+ iTelephony.setAllowedNetworkTypesForReason(subId, reasonUser, networkTypeBitmask)
} else {
- if (enabled) {
- // Set pure 5G mode
- iTelephony.setPreferredNetworkType(subId, modeNrOnly)
- } else {
- // Set pure 4G mode
- iTelephony.setPreferredNetworkType(subId, modeLteOnly)
- }
- }
- } catch (e: Exception) {
- // If pure modes fail, try with basic fallbacks
- try {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- val fallbackTypes = if (enabled) typeNrOnly or typeLteOnly else typeLteOnly
- iTelephony.setAllowedNetworkTypesForReason(subId, reasonUser, fallbackTypes)
- } else {
- val fallbackMode = if (enabled) modeNrOnly else modeLteOnly
- iTelephony.setPreferredNetworkType(subId, fallbackMode)
- }
- } catch (fallbackException: Exception) {
- throw Exception("Failed to set network mode: ${fallbackException.message}")
+ iTelephony.setPreferredNetworkType(subId, networkMode)
}
+ } catch (_: Exception) {
+ // fail
}
}
}
diff --git a/app/src/main/java/com/supernova/networkswitch/service/ShizukuControllerService.kt b/app/src/main/java/com/supernova/networkswitch/service/ShizukuControllerService.kt
index b1d3920..1408b64 100644
--- a/app/src/main/java/com/supernova/networkswitch/service/ShizukuControllerService.kt
+++ b/app/src/main/java/com/supernova/networkswitch/service/ShizukuControllerService.kt
@@ -9,7 +9,7 @@ import com.android.internal.telephony.ITelephony
import com.supernova.networkswitch.IShizukuController
/**
- * Shizuku service for network control operations
+ * Simple Shizuku service for network control operations
*/
class ShizukuControllerService() : IShizukuController.Stub() {
@@ -24,68 +24,230 @@ class ShizukuControllerService() : IShizukuController.Stub() {
.getInt(null)
}
- private val typeNr by lazy {
+ // Get network type bitmasks from Android TelephonyManager constants
+ private val NETWORK_TYPE_BITMASK_GSM by lazy {
Class.forName("android.telephony.TelephonyManager")
- .getDeclaredField("NETWORK_TYPE_BITMASK_NR")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_GSM")
.getLong(null)
}
-
- @delegate:SuppressLint("PrivateApi", "BlockedPrivateApi")
- private val modeLte by lazy {
- Class.forName("com.android.internal.telephony.RILConstants")
- .getDeclaredField("NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA")
- .getInt(null)
+
+ private val NETWORK_TYPE_BITMASK_GPRS by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_GPRS")
+ .getLong(null)
}
-
- @delegate:SuppressLint("PrivateApi", "BlockedPrivateApi")
- private val modeNr by lazy {
- Class.forName("com.android.internal.telephony.RILConstants")
- .getDeclaredField("NETWORK_MODE_NR_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA")
- .getInt(null)
+
+ private val NETWORK_TYPE_BITMASK_EDGE by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_EDGE")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_UMTS by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_UMTS")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_CDMA by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_CDMA")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_EVDO_0 by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_EVDO_0")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_EVDO_A by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_EVDO_A")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_1xRTT by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_1xRTT")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_HSDPA by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_HSDPA")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_HSUPA by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_HSUPA")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_HSPA by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_HSPA")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_EVDO_B by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_EVDO_B")
+ .getLong(null)
}
- // Pure mode constants
- @delegate:SuppressLint("PrivateApi", "BlockedPrivateApi")
- private val typeLteOnly: Long by lazy {
- try {
- Class.forName("android.telephony.TelephonyManager")
- .getDeclaredField("NETWORK_TYPE_BITMASK_LTE")
- .getLong(null)
+ private val NETWORK_TYPE_BITMASK_LTE by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_LTE")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_EHRPD by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_EHRPD")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_HSPAP by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_HSPAP")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_TD_SCDMA by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_TD_SCDMA")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_LTE_CA by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_LTE_CA")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_IWLAN by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_IWLAN")
+ .getLong(null)
+ }
+
+ private val NETWORK_TYPE_BITMASK_NR by lazy {
+ Class.forName("android.telephony.TelephonyManager")
+ .getDeclaredField("NETWORK_TYPE_BITMASK_NR")
+ .getLong(null)
+ }
+
+ // Combined bitmasks for common network classes
+ private fun get2GBitmask(): Long {
+ return try {
+ NETWORK_TYPE_BITMASK_GSM or NETWORK_TYPE_BITMASK_GPRS or
+ NETWORK_TYPE_BITMASK_EDGE or NETWORK_TYPE_BITMASK_CDMA or NETWORK_TYPE_BITMASK_1xRTT
} catch (e: Exception) {
- 524288L // Fallback LTE bitmask
+ 1L or 2L or 4L or 16L or 128L
}
}
-
- @delegate:SuppressLint("PrivateApi", "BlockedPrivateApi")
- private val typeNrOnly: Long by lazy {
- try {
- Class.forName("android.telephony.TelephonyManager")
- .getDeclaredField("NETWORK_TYPE_BITMASK_NR")
- .getLong(null)
+
+ private fun get3GBitmask(): Long {
+ return try {
+ NETWORK_TYPE_BITMASK_EVDO_0 or NETWORK_TYPE_BITMASK_EVDO_A or
+ NETWORK_TYPE_BITMASK_EVDO_B or NETWORK_TYPE_BITMASK_EHRPD or
+ NETWORK_TYPE_BITMASK_HSUPA or NETWORK_TYPE_BITMASK_HSDPA or
+ NETWORK_TYPE_BITMASK_HSPA or NETWORK_TYPE_BITMASK_HSPAP or
+ NETWORK_TYPE_BITMASK_UMTS or NETWORK_TYPE_BITMASK_TD_SCDMA
} catch (e: Exception) {
- 2097152L // Fallback NR bitmask
+ 32L or 64L or 2048L or 8192L or 512L or 256L or 1024L or 16384L or 8L or 32768L
}
}
-
- @delegate:SuppressLint("PrivateApi", "BlockedPrivateApi")
- private val modeLteOnly: Int by lazy {
- try {
- Class.forName("com.android.internal.telephony.RILConstants")
- .getDeclaredField("NETWORK_MODE_LTE_ONLY")
- .getInt(null)
+
+ private fun get4GBitmask(): Long {
+ return try {
+ NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_LTE_CA or NETWORK_TYPE_BITMASK_IWLAN
} catch (e: Exception) {
- 11 // Fallback value
+ 4096L or 65536L or 131072L
}
}
-
- @delegate:SuppressLint("PrivateApi", "BlockedPrivateApi")
- private val modeNrOnly: Int by lazy {
- try {
- Class.forName("com.android.internal.telephony.RILConstants")
- .getDeclaredField("NETWORK_MODE_NR_ONLY")
- .getInt(null)
+
+ private fun get5GBitmask(): Long {
+ return try {
+ NETWORK_TYPE_BITMASK_NR
} catch (e: Exception) {
- 23 // Fallback value
+ 524288L
+ }
+ }
+
+ /**
+ * Map RIL network mode constants to network type bitmasks for Android 12+
+ */
+ private fun mapNetworkModeToBitmask(networkMode: Int): Long {
+ return when (networkMode) {
+ 0 -> get2GBitmask() or get3GBitmask() // WCDMA_PREF
+ 1 -> NETWORK_TYPE_BITMASK_GSM // GSM_ONLY
+ 2 -> NETWORK_TYPE_BITMASK_UMTS // WCDMA_ONLY
+ 3 -> get2GBitmask() or get3GBitmask() // GSM_UMTS
+ 4 -> NETWORK_TYPE_BITMASK_CDMA or NETWORK_TYPE_BITMASK_EVDO_0 or NETWORK_TYPE_BITMASK_EVDO_A or NETWORK_TYPE_BITMASK_EVDO_B // CDMA
+ 5 -> NETWORK_TYPE_BITMASK_CDMA // CDMA_NO_EVDO
+ 6 -> NETWORK_TYPE_BITMASK_EVDO_0 or NETWORK_TYPE_BITMASK_EVDO_A or NETWORK_TYPE_BITMASK_EVDO_B // EVDO_NO_CDMA
+ 7 -> get2GBitmask() or get3GBitmask() or get4GBitmask() // GLOBAL
+ 8 -> NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_CDMA or NETWORK_TYPE_BITMASK_EVDO_0 or NETWORK_TYPE_BITMASK_EVDO_A or NETWORK_TYPE_BITMASK_EVDO_B // LTE_CDMA_EVDO
+ 9 -> NETWORK_TYPE_BITMASK_LTE or get2GBitmask() or get3GBitmask() // LTE_GSM_WCDMA
+ 10 -> NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_CDMA or NETWORK_TYPE_BITMASK_EVDO_0 or NETWORK_TYPE_BITMASK_EVDO_A or NETWORK_TYPE_BITMASK_EVDO_B or get2GBitmask() or get3GBitmask() // LTE_CDMA_EVDO_GSM_WCDMA
+ 11 -> NETWORK_TYPE_BITMASK_LTE // LTE_ONLY
+ 12 -> NETWORK_TYPE_BITMASK_LTE or get3GBitmask() // LTE_WCDMA
+ 13 -> NETWORK_TYPE_BITMASK_TD_SCDMA // TDSCDMA_ONLY
+ 14 -> NETWORK_TYPE_BITMASK_TD_SCDMA or NETWORK_TYPE_BITMASK_UMTS // TDSCDMA_WCDMA
+ 15 -> NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_TD_SCDMA // LTE_TDSCDMA
+ 16 -> NETWORK_TYPE_BITMASK_TD_SCDMA or get2GBitmask() // TDSCDMA_GSM
+ 17 -> NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_TD_SCDMA or get2GBitmask() // LTE_TDSCDMA_GSM
+ 18 -> NETWORK_TYPE_BITMASK_TD_SCDMA or get2GBitmask() or get3GBitmask() // TDSCDMA_GSM_WCDMA
+ 19 -> NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_TD_SCDMA or get3GBitmask() // LTE_TDSCDMA_WCDMA
+ 20 -> NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_TD_SCDMA or get2GBitmask() or get3GBitmask() // LTE_TDSCDMA_GSM_WCDMA
+ 21 -> NETWORK_TYPE_BITMASK_TD_SCDMA or NETWORK_TYPE_BITMASK_CDMA or NETWORK_TYPE_BITMASK_EVDO_0 or NETWORK_TYPE_BITMASK_EVDO_A or NETWORK_TYPE_BITMASK_EVDO_B or get2GBitmask() or get3GBitmask() // TDSCDMA_CDMA_EVDO_GSM_WCDMA
+ 22 -> NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_TD_SCDMA or NETWORK_TYPE_BITMASK_CDMA or NETWORK_TYPE_BITMASK_EVDO_0 or NETWORK_TYPE_BITMASK_EVDO_A or NETWORK_TYPE_BITMASK_EVDO_B or get2GBitmask() or get3GBitmask() // LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA
+ 23 -> NETWORK_TYPE_BITMASK_NR // NR_ONLY
+ 24 -> NETWORK_TYPE_BITMASK_NR or NETWORK_TYPE_BITMASK_LTE // NR_LTE
+ 25 -> NETWORK_TYPE_BITMASK_NR or NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_CDMA or NETWORK_TYPE_BITMASK_EVDO_0 or NETWORK_TYPE_BITMASK_EVDO_A or NETWORK_TYPE_BITMASK_EVDO_B // NR_LTE_CDMA_EVDO
+ 26 -> NETWORK_TYPE_BITMASK_NR or NETWORK_TYPE_BITMASK_LTE or get2GBitmask() or get3GBitmask() // NR_LTE_GSM_WCDMA
+ 27 -> NETWORK_TYPE_BITMASK_NR or NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_CDMA or NETWORK_TYPE_BITMASK_EVDO_0 or NETWORK_TYPE_BITMASK_EVDO_A or NETWORK_TYPE_BITMASK_EVDO_B or get2GBitmask() or get3GBitmask() // NR_LTE_CDMA_EVDO_GSM_WCDMA
+ 28 -> NETWORK_TYPE_BITMASK_NR or NETWORK_TYPE_BITMASK_LTE or get3GBitmask() // NR_LTE_WCDMA
+ 29 -> NETWORK_TYPE_BITMASK_NR or NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_TD_SCDMA // NR_LTE_TDSCDMA
+ 30 -> NETWORK_TYPE_BITMASK_NR or NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_TD_SCDMA or get2GBitmask() // NR_LTE_TDSCDMA_GSM
+ 31 -> NETWORK_TYPE_BITMASK_NR or NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_TD_SCDMA or get3GBitmask() // NR_LTE_TDSCDMA_WCDMA
+ 32 -> NETWORK_TYPE_BITMASK_NR or NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_TD_SCDMA or get2GBitmask() or get3GBitmask() // NR_LTE_TDSCDMA_GSM_WCDMA
+ 33 -> NETWORK_TYPE_BITMASK_NR or NETWORK_TYPE_BITMASK_LTE or NETWORK_TYPE_BITMASK_TD_SCDMA or NETWORK_TYPE_BITMASK_CDMA or NETWORK_TYPE_BITMASK_EVDO_0 or NETWORK_TYPE_BITMASK_EVDO_A or NETWORK_TYPE_BITMASK_EVDO_B or get2GBitmask() or get3GBitmask() // NR_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA
+ else -> get2GBitmask() or get3GBitmask() or get4GBitmask() // Default fallback
+ }
+ }
+
+ /**
+ * Map bitmask back to RIL network mode for getCurrentNetworkMode
+ */
+ private fun mapBitmaskToNetworkMode(bitmask: Long): Int {
+ // Try exact matches for all modes by comparing with their expected bitmasks
+ for (mode in 0..33) {
+ try {
+ if (bitmask == mapNetworkModeToBitmask(mode)) {
+ return mode
+ }
+ } catch (_: Exception) {
+ // Skip invalid modes
+ }
+ }
+
+ // If no exact match found, use simple fallback logic
+ return when {
+ bitmask == NETWORK_TYPE_BITMASK_NR -> 23 // NR_ONLY
+ bitmask == NETWORK_TYPE_BITMASK_LTE -> 11 // LTE_ONLY
+ bitmask == NETWORK_TYPE_BITMASK_GSM -> 1 // GSM_ONLY
+ bitmask == NETWORK_TYPE_BITMASK_UMTS -> 2 // WCDMA_ONLY
+ bitmask == NETWORK_TYPE_BITMASK_TD_SCDMA -> 13 // TDSCDMA_ONLY
+ bitmask == (NETWORK_TYPE_BITMASK_NR or NETWORK_TYPE_BITMASK_LTE) -> 24 // NR_LTE
+ (bitmask and NETWORK_TYPE_BITMASK_NR) != 0L -> 23 // Has 5G, default to NR_ONLY
+ (bitmask and NETWORK_TYPE_BITMASK_LTE) != 0L -> 11 // Has LTE, default to LTE_ONLY
+ (bitmask and get3GBitmask()) != 0L -> 2 // Has 3G, default to WCDMA_ONLY
+ (bitmask and get2GBitmask()) != 0L -> 1 // Has 2G, default to GSM_ONLY
+ else -> 0 // WCDMA_PREF as ultimate fallback
}
}
}
@@ -96,71 +258,44 @@ class ShizukuControllerService() : IShizukuController.Stub() {
override fun compatibilityCheck(subId: Int): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- // Test pure modes on Android 12+
- val originalTypes = iTelephony.getAllowedNetworkTypesForReason(subId, reasonUser)
-
- // Test LTE-only
- iTelephony.setAllowedNetworkTypesForReason(subId, reasonUser, typeLteOnly)
- val lteTest = iTelephony.getAllowedNetworkTypesForReason(subId, reasonUser)
-
- // Test NR-only
- iTelephony.setAllowedNetworkTypesForReason(subId, reasonUser, typeNrOnly)
- val nrTest = iTelephony.getAllowedNetworkTypesForReason(subId, reasonUser)
-
- // Restore original
- iTelephony.setAllowedNetworkTypesForReason(subId, reasonUser, originalTypes)
-
- (lteTest == typeLteOnly) && (nrTest == typeNrOnly)
+ reasonUser
+ iTelephony.setAllowedNetworkTypesForReason(
+ subId,
+ reasonUser,
+ iTelephony.getAllowedNetworkTypesForReason(subId, reasonUser)
+ )
} else {
- // Test preferred network modes for older Android
- val original = iTelephony.getPreferredNetworkType(subId)
-
- iTelephony.setPreferredNetworkType(subId, modeLteOnly)
- val lteTest = iTelephony.getPreferredNetworkType(subId) == modeLteOnly
-
- iTelephony.setPreferredNetworkType(subId, modeNrOnly)
- val nrTest = iTelephony.getPreferredNetworkType(subId) == modeNrOnly
-
- // Restore
- iTelephony.setPreferredNetworkType(subId, original)
-
- lteTest && nrTest
+ iTelephony.setPreferredNetworkType(
+ subId,
+ iTelephony.getPreferredNetworkType(subId)
+ )
}
+ true
} catch (_: Exception) {
false
}
}
- override fun getNetworkState(subId: Int): Boolean {
+ override fun getCurrentNetworkMode(subId: Int): Int {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- iTelephony.getAllowedNetworkTypesForReason(subId, reasonUser) and typeNr != 0L
+ val currentBitmask = iTelephony.getAllowedNetworkTypesForReason(subId, reasonUser)
+ mapBitmaskToNetworkMode(currentBitmask)
} else {
- val currentMode = iTelephony.getPreferredNetworkType(subId)
- currentMode == modeNrOnly || currentMode >= 23 // 5G modes
+ iTelephony.getPreferredNetworkType(subId)
}
} catch (_: Exception) {
- false
+ -1
}
}
- override fun setNetworkState(subId: Int, enabled: Boolean) {
+ override fun setNetworkMode(subId: Int, networkMode: Int) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- if (enabled) {
- // Use pure 5G mode (NR only)
- iTelephony.setAllowedNetworkTypesForReason(subId, reasonUser, typeNrOnly)
- } else {
- // Use pure 4G mode (LTE only)
- iTelephony.setAllowedNetworkTypesForReason(subId, reasonUser, typeLteOnly)
- }
+ val networkTypeBitmask = mapNetworkModeToBitmask(networkMode)
+ iTelephony.setAllowedNetworkTypesForReason(subId, reasonUser, networkTypeBitmask)
} else {
- // For older Android versions
- if (enabled) {
- iTelephony.setPreferredNetworkType(subId, modeNrOnly)
- } else {
- iTelephony.setPreferredNetworkType(subId, modeLteOnly)
- }
+ iTelephony.setPreferredNetworkType(subId, networkMode)
}
} catch (_: Exception) {
// Silently fail
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8fba76e..7801869 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,32 +1,12 @@
Network Switch
- Hint
- Now you can add the tile to the quick settings and toggle between pure 4G and pure 5G modes. The app uses only LTE and NR modes without fallbacks for maximum performance.
- Shizuku permission is needed for this app to work properly. Please install Shizuku and grant permission.
- Checking…
- Root not granted
- Shizuku permission not granted
- Not compatible
- Compatible
- About
- Source Code
- Open Source Licenses
- 4G/5G Toggle
- Root permission is needed for this app to work properly :(
- Sorry but this app is not compatible with your system. Consider to open an issue in the Github repository with enough information :)
+ Network Switch
-
- Checking compatibility…
- Device Compatible
- Your device supports network switching
- Device Not Compatible
- Network Mode
- Add the \"4G/5G Toggle\" tile to your Quick Settings for instant network switching. Pull down your notification panel, tap the pencil icon, and add the tile.
- Settings
- Control Method
- Choose how the app should control network settings. Root method requires a rooted device, while Shizuku method works with non-rooted devices that have Shizuku installed.
- Root Method
- Requires rooted device with root access granted
- Shizuku Method
- Works with non-rooted devices using Shizuku service
+
+ Network Mode Configuration
+ Choose which two network modes to toggle between
+ Mode A
+ Mode B
+ Save Configuration
+ Configuration saved successfully
\ No newline at end of file
diff --git a/app/src/test/java/com/supernova/networkswitch/ExampleUnitTest.kt b/app/src/test/java/com/supernova/networkswitch/ExampleUnitTest.kt
index 8de7c23..11c3f76 100644
--- a/app/src/test/java/com/supernova/networkswitch/ExampleUnitTest.kt
+++ b/app/src/test/java/com/supernova/networkswitch/ExampleUnitTest.kt
@@ -5,9 +5,6 @@ import org.junit.Assert.*
import com.supernova.networkswitch.domain.model.ControlMethod
import com.supernova.networkswitch.domain.model.NetworkMode
-/**
- * Unit tests for the Network Switch application domain models.
- */
class NetworkSwitchUnitTest {
@Test
@@ -19,22 +16,19 @@ class NetworkSwitchUnitTest {
}
@Test
- fun networkMode_enum_values_are_correct() {
+ fun networkMode_enum_values_contain_expected_modes() {
val modes = NetworkMode.values()
- assertEquals(2, modes.size)
- assertTrue(modes.contains(NetworkMode.FOUR_G_ONLY))
- assertTrue(modes.contains(NetworkMode.FIVE_G_ONLY))
+ assertTrue(modes.size > 10)
+ assertTrue(modes.contains(NetworkMode.LTE_ONLY))
+ assertTrue(modes.contains(NetworkMode.NR_ONLY))
+ assertTrue(modes.contains(NetworkMode.GSM_ONLY))
+ assertTrue(modes.contains(NetworkMode.WCDMA_ONLY))
}
@Test
- fun networkMode_toggle_logic_works() {
- var currentMode = NetworkMode.FOUR_G_ONLY
- currentMode = if (currentMode == NetworkMode.FOUR_G_ONLY)
- NetworkMode.FIVE_G_ONLY else NetworkMode.FOUR_G_ONLY
- assertEquals(NetworkMode.FIVE_G_ONLY, currentMode)
-
- currentMode = if (currentMode == NetworkMode.FOUR_G_ONLY)
- NetworkMode.FIVE_G_ONLY else NetworkMode.FOUR_G_ONLY
- assertEquals(NetworkMode.FOUR_G_ONLY, currentMode)
+ fun networkMode_fromValue_works_correctly() {
+ assertEquals(NetworkMode.LTE_ONLY, NetworkMode.fromValue(11))
+ assertEquals(NetworkMode.NR_ONLY, NetworkMode.fromValue(23))
+ assertNull(NetworkMode.fromValue(-1))
}
}
\ No newline at end of file
diff --git a/app/src/test/java/com/supernova/networkswitch/data/repository/NetworkControlRepositoryImplTest.kt b/app/src/test/java/com/supernova/networkswitch/data/repository/NetworkControlRepositoryImplTest.kt
deleted file mode 100644
index 24ee899..0000000
--- a/app/src/test/java/com/supernova/networkswitch/data/repository/NetworkControlRepositoryImplTest.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-package com.supernova.networkswitch.data.repository
-
-import com.supernova.networkswitch.data.source.RootNetworkControlDataSource
-import com.supernova.networkswitch.data.source.ShizukuNetworkControlDataSource
-import com.supernova.networkswitch.domain.model.ControlMethod
-import com.supernova.networkswitch.domain.repository.PreferencesRepository
-import com.supernova.networkswitch.util.CoroutineTestRule
-import io.mockk.coEvery
-import io.mockk.coVerify
-import io.mockk.mockk
-import android.telephony.SubscriptionManager
-import io.mockk.every
-import io.mockk.mockkStatic
-import io.mockk.unmockkAll
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-
-@ExperimentalCoroutinesApi
-class NetworkControlRepositoryImplTest {
-
- @get:Rule
- val coroutineTestRule = CoroutineTestRule()
-
- private lateinit var rootDataSource: RootNetworkControlDataSource
- private lateinit var shizukuDataSource: ShizukuNetworkControlDataSource
- private lateinit var preferencesRepository: PreferencesRepository
- private lateinit var repository: NetworkControlRepositoryImpl
-
- @Before
- fun setUp() {
- rootDataSource = mockk(relaxed = true)
- shizukuDataSource = mockk(relaxed = true)
- preferencesRepository = mockk(relaxed = true)
-
- mockkStatic(SubscriptionManager::class)
- every { SubscriptionManager.getDefaultDataSubscriptionId() } returns 1
-
- repository = NetworkControlRepositoryImpl(
- rootDataSource,
- shizukuDataSource,
- preferencesRepository
- )
- }
-
- @After
- fun tearDown() {
- unmockkAll()
- }
-
- @Test
- fun `checkCompatibility uses RootDataSource when method is ROOT`() = runTest {
- coEvery { preferencesRepository.getControlMethod() } returns ControlMethod.ROOT
-
- repository.checkCompatibility(ControlMethod.ROOT)
-
- coVerify { rootDataSource.checkCompatibility(1) }
- coVerify(exactly = 0) { shizukuDataSource.checkCompatibility(any()) }
- }
-
- @Test
- fun `checkCompatibility uses ShizukuDataSource when method is SHIZUKU`() = runTest {
- coEvery { preferencesRepository.getControlMethod() } returns ControlMethod.SHIZUKU
-
- repository.checkCompatibility(ControlMethod.SHIZUKU)
-
- coVerify { shizukuDataSource.checkCompatibility(1) }
- coVerify(exactly = 0) { rootDataSource.checkCompatibility(any()) }
- }
-
- @Test
- fun `getNetworkState uses RootDataSource when method is ROOT`() = runTest {
- coEvery { preferencesRepository.getControlMethod() } returns ControlMethod.ROOT
- val subId = 1
-
- repository.getNetworkState(subId)
-
- coVerify { rootDataSource.getNetworkState(subId) }
- coVerify(exactly = 0) { shizukuDataSource.getNetworkState(any()) }
- }
-
- @Test
- fun `getNetworkState uses ShizukuDataSource when method is SHIZUKU`() = runTest {
- coEvery { preferencesRepository.getControlMethod() } returns ControlMethod.SHIZUKU
- val subId = 1
-
- repository.getNetworkState(subId)
-
- coVerify { shizukuDataSource.getNetworkState(subId) }
- coVerify(exactly = 0) { rootDataSource.getNetworkState(any()) }
- }
-
- @Test
- fun `setNetworkState uses RootDataSource when method is ROOT`() = runTest {
- coEvery { preferencesRepository.getControlMethod() } returns ControlMethod.ROOT
- val subId = 1
- val enabled = true
-
- repository.setNetworkState(subId, enabled)
-
- coVerify { rootDataSource.setNetworkState(subId, enabled) }
- coVerify(exactly = 0) { shizukuDataSource.setNetworkState(any(), any()) }
- }
-
- @Test
- fun `setNetworkState uses ShizukuDataSource when method is SHIZUKU`() = runTest {
- coEvery { preferencesRepository.getControlMethod() } returns ControlMethod.SHIZUKU
- val subId = 1
- val enabled = true
-
- repository.setNetworkState(subId, enabled)
-
- coVerify { shizukuDataSource.setNetworkState(subId, enabled) }
- coVerify(exactly = 0) { rootDataSource.setNetworkState(any(), any()) }
- }
-
- @Test
- fun `resetConnections calls reset on both data sources`() = runTest {
- repository.resetConnections()
-
- coVerify { rootDataSource.resetConnection() }
- coVerify { shizukuDataSource.resetConnection() }
- }
-}
diff --git a/app/src/test/java/com/supernova/networkswitch/domain/model/NetworkModeTest.kt b/app/src/test/java/com/supernova/networkswitch/domain/model/NetworkModeTest.kt
new file mode 100644
index 0000000..9b23da1
--- /dev/null
+++ b/app/src/test/java/com/supernova/networkswitch/domain/model/NetworkModeTest.kt
@@ -0,0 +1,36 @@
+package com.supernova.networkswitch.domain.model
+
+import org.junit.Assert.*
+import org.junit.Test
+
+/**
+ * Unit tests for NetworkMode enum
+ */
+class NetworkModeTest {
+
+ @Test
+ fun testNetworkModeFromValue() {
+ // Test basic modes
+ assertEquals(NetworkMode.GSM_ONLY, NetworkMode.fromValue(1))
+ assertEquals(NetworkMode.WCDMA_ONLY, NetworkMode.fromValue(2))
+ assertEquals(NetworkMode.LTE_ONLY, NetworkMode.fromValue(11))
+ assertEquals(NetworkMode.NR_ONLY, NetworkMode.fromValue(23))
+
+ // Test combined modes
+ assertEquals(NetworkMode.NR_LTE, NetworkMode.fromValue(24))
+ assertEquals(NetworkMode.NR_LTE_GSM_WCDMA, NetworkMode.fromValue(26))
+
+ // Test invalid value
+ assertNull(NetworkMode.fromValue(-1))
+ assertNull(NetworkMode.fromValue(999))
+ }
+
+ @Test
+ fun testNetworkModeDisplayNames() {
+ assertEquals("2G Only (GSM)", NetworkMode.GSM_ONLY.displayName)
+ assertEquals("3G Only (WCDMA)", NetworkMode.WCDMA_ONLY.displayName)
+ assertEquals("4G Only (LTE)", NetworkMode.LTE_ONLY.displayName)
+ assertEquals("5G Only (NR)", NetworkMode.NR_ONLY.displayName)
+ assertEquals("4G/5G (NR/LTE)", NetworkMode.NR_LTE.displayName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/supernova/networkswitch/domain/model/ToggleModeConfigTest.kt b/app/src/test/java/com/supernova/networkswitch/domain/model/ToggleModeConfigTest.kt
new file mode 100644
index 0000000..3e10cdd
--- /dev/null
+++ b/app/src/test/java/com/supernova/networkswitch/domain/model/ToggleModeConfigTest.kt
@@ -0,0 +1,70 @@
+package com.supernova.networkswitch.domain.model
+
+import org.junit.Assert.*
+import org.junit.Test
+
+/**
+ * Unit tests for ToggleModeConfig
+ */
+class ToggleModeConfigTest {
+
+ @Test
+ fun testGetNextMode() {
+ val config = ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY, nextModeIsB = true)
+
+ // When nextModeIsB = true, should return modeB
+ assertEquals(NetworkMode.NR_ONLY, config.getNextMode())
+
+ val config2 = ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY, nextModeIsB = false)
+
+ // When nextModeIsB = false, should return modeA
+ assertEquals(NetworkMode.LTE_ONLY, config2.getNextMode())
+ }
+
+ @Test
+ fun testGetCurrentMode() {
+ val config = ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY, nextModeIsB = true)
+
+ // When nextModeIsB = true, current should be modeA
+ assertEquals(NetworkMode.LTE_ONLY, config.getCurrentMode())
+
+ val config2 = ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY, nextModeIsB = false)
+
+ // When nextModeIsB = false, current should be modeB
+ assertEquals(NetworkMode.NR_ONLY, config2.getCurrentMode())
+ }
+
+ @Test
+ fun testToggle() {
+ val config = ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY, nextModeIsB = true)
+
+ val toggledConfig = config.toggle()
+
+ // Should flip the nextModeIsB flag
+ assertFalse(toggledConfig.nextModeIsB)
+ assertEquals(NetworkMode.LTE_ONLY, toggledConfig.modeA)
+ assertEquals(NetworkMode.NR_ONLY, toggledConfig.modeB)
+ }
+
+ @Test
+ fun testAlternatingToggle() {
+ val config = ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY, nextModeIsB = true)
+
+ // First toggle: next is NR_ONLY, current is LTE_ONLY
+ assertEquals(NetworkMode.NR_ONLY, config.getNextMode())
+ assertEquals(NetworkMode.LTE_ONLY, config.getCurrentMode())
+
+ val afterFirstToggle = config.toggle()
+
+ // After toggle: next is LTE_ONLY, current is NR_ONLY
+ assertEquals(NetworkMode.LTE_ONLY, afterFirstToggle.getNextMode())
+ assertEquals(NetworkMode.NR_ONLY, afterFirstToggle.getCurrentMode())
+
+ val afterSecondToggle = afterFirstToggle.toggle()
+
+ // After second toggle: back to original state
+ assertEquals(config.nextModeIsB, afterSecondToggle.nextModeIsB)
+ assertEquals(NetworkMode.NR_ONLY, afterSecondToggle.getNextMode())
+ assertEquals(NetworkMode.LTE_ONLY, afterSecondToggle.getCurrentMode())
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/supernova/networkswitch/domain/usecase/NetworkUseCasesTest.kt b/app/src/test/java/com/supernova/networkswitch/domain/usecase/NetworkUseCasesTest.kt
deleted file mode 100644
index 0fa7950..0000000
--- a/app/src/test/java/com/supernova/networkswitch/domain/usecase/NetworkUseCasesTest.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-package com.supernova.networkswitch.domain.usecase
-
-import com.supernova.networkswitch.domain.model.CompatibilityState
-import com.supernova.networkswitch.domain.model.ControlMethod
-import com.supernova.networkswitch.domain.repository.NetworkControlRepository
-import com.supernova.networkswitch.domain.repository.PreferencesRepository
-import com.supernova.networkswitch.util.CoroutineTestRule
-import io.mockk.coEvery
-import io.mockk.coVerify
-import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-
-@ExperimentalCoroutinesApi
-class NetworkUseCasesTest {
-
- @get:Rule
- val coroutineTestRule = CoroutineTestRule()
-
- private lateinit var networkControlRepository: NetworkControlRepository
- private lateinit var preferencesRepository: PreferencesRepository
-
- private lateinit var checkCompatibilityUseCase: CheckCompatibilityUseCase
- private lateinit var toggleNetworkModeUseCase: ToggleNetworkModeUseCase
- private lateinit var getNetworkStateUseCase: GetNetworkStateUseCase
- private lateinit var updateControlMethodUseCase: UpdateControlMethodUseCase
- private lateinit var resetConnectionsUseCase: ResetConnectionsUseCase
-
- @Before
- fun setUp() {
- networkControlRepository = mockk(relaxed = true)
- preferencesRepository = mockk(relaxed = true)
-
- checkCompatibilityUseCase = CheckCompatibilityUseCase(networkControlRepository, preferencesRepository)
- toggleNetworkModeUseCase = ToggleNetworkModeUseCase(networkControlRepository)
- getNetworkStateUseCase = GetNetworkStateUseCase(networkControlRepository)
- updateControlMethodUseCase = UpdateControlMethodUseCase(preferencesRepository)
- resetConnectionsUseCase = ResetConnectionsUseCase(networkControlRepository)
- }
-
- @Test
- fun `CheckCompatibilityUseCase returns state from repository`() = runTest {
- val expectedState = CompatibilityState.Compatible
- coEvery { preferencesRepository.getControlMethod() } returns ControlMethod.ROOT
- coEvery { networkControlRepository.checkCompatibility(any()) } returns expectedState
-
- val result = checkCompatibilityUseCase()
-
- assertEquals(expectedState, result)
- }
-
- @Test
- fun `ToggleNetworkModeUseCase toggles from true to false`() = runTest {
- coEvery { networkControlRepository.getNetworkState(any()) } returns true
- coEvery { networkControlRepository.setNetworkState(any(), false) } returns Result.success(Unit)
-
- val result = toggleNetworkModeUseCase(1)
-
- coVerify { networkControlRepository.setNetworkState(1, false) }
- assertTrue(result.isSuccess)
- assertEquals(false, result.getOrNull())
- }
-
- @Test
- fun `ToggleNetworkModeUseCase toggles from false to true`() = runTest {
- coEvery { networkControlRepository.getNetworkState(any()) } returns false
- coEvery { networkControlRepository.setNetworkState(any(), true) } returns Result.success(Unit)
-
- val result = toggleNetworkModeUseCase(1)
-
- coVerify { networkControlRepository.setNetworkState(1, true) }
- assertTrue(result.isSuccess)
- assertEquals(true, result.getOrNull())
- }
-
- @Test
- fun `ToggleNetworkModeUseCase handles failure`() = runTest {
- val exception = RuntimeException("Test Exception")
- coEvery { networkControlRepository.getNetworkState(any()) } throws exception
-
- val result = toggleNetworkModeUseCase(1)
-
- assertTrue(result.isFailure)
- assertEquals(exception, result.exceptionOrNull())
- }
-
- @Test
- fun `GetNetworkStateUseCase returns state from repository`() = runTest {
- coEvery { networkControlRepository.getNetworkState(any()) } returns true
-
- val result = getNetworkStateUseCase(1)
-
- assertTrue(result.isSuccess)
- assertEquals(true, result.getOrNull())
- }
-
- @Test
- fun `GetNetworkStateUseCase handles failure`() = runTest {
- val exception = RuntimeException("Test Exception")
- coEvery { networkControlRepository.getNetworkState(any()) } throws exception
-
- val result = getNetworkStateUseCase(1)
-
- assertTrue(result.isFailure)
- assertEquals(exception, result.exceptionOrNull())
- }
-
- @Test
- fun `UpdateControlMethodUseCase calls repository`() = runTest {
- val method = ControlMethod.SHIZUKU
- updateControlMethodUseCase(method)
- coVerify { preferencesRepository.setControlMethod(method) }
- }
-
- @Test
- fun `ResetConnectionsUseCase calls repository`() = runTest {
- resetConnectionsUseCase()
- coVerify { networkControlRepository.resetConnections() }
- }
-}
diff --git a/app/src/test/java/com/supernova/networkswitch/domain/usecase/ToggleNetworkModeUseCaseTest.kt b/app/src/test/java/com/supernova/networkswitch/domain/usecase/ToggleNetworkModeUseCaseTest.kt
new file mode 100644
index 0000000..8d53ff7
--- /dev/null
+++ b/app/src/test/java/com/supernova/networkswitch/domain/usecase/ToggleNetworkModeUseCaseTest.kt
@@ -0,0 +1,134 @@
+package com.supernova.networkswitch.domain.usecase
+
+import com.supernova.networkswitch.domain.model.NetworkMode
+import com.supernova.networkswitch.domain.model.ToggleModeConfig
+import com.supernova.networkswitch.domain.repository.NetworkControlRepository
+import com.supernova.networkswitch.domain.repository.PreferencesRepository
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * Unit tests for ToggleNetworkModeUseCase with alternating logic
+ */
+class ToggleNetworkModeUseCaseTest {
+
+ private lateinit var networkControlRepository: NetworkControlRepository
+ private lateinit var preferencesRepository: PreferencesRepository
+ private lateinit var useCase: ToggleNetworkModeUseCase
+
+ private val testSubId = 1
+
+ @Before
+ fun setUp() {
+ networkControlRepository = mockk()
+ preferencesRepository = mockk()
+ useCase = ToggleNetworkModeUseCase(networkControlRepository, preferencesRepository)
+ }
+
+ @Test
+ fun `should switch to mode B when nextModeIsB is true`() = runTest {
+ // Given
+ val config = ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY, nextModeIsB = true)
+ coEvery { preferencesRepository.getToggleModeConfig() } returns config
+ coEvery { networkControlRepository.setNetworkMode(testSubId, NetworkMode.NR_ONLY) } returns Result.success(Unit)
+
+ val updatedConfig = config.toggle() // After successful switch, toggle the state
+ coEvery { preferencesRepository.setToggleModeConfig(updatedConfig) } returns Unit
+
+ // When
+ val result = useCase(testSubId)
+
+ // Then
+ assertTrue(result.isSuccess)
+ assertEquals(NetworkMode.NR_ONLY, result.getOrNull())
+ coVerify { networkControlRepository.setNetworkMode(testSubId, NetworkMode.NR_ONLY) }
+ coVerify { preferencesRepository.setToggleModeConfig(updatedConfig) }
+ }
+
+ @Test
+ fun `should switch to mode A when nextModeIsB is false`() = runTest {
+ // Given
+ val config = ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY, nextModeIsB = false)
+ coEvery { preferencesRepository.getToggleModeConfig() } returns config
+ coEvery { networkControlRepository.setNetworkMode(testSubId, NetworkMode.LTE_ONLY) } returns Result.success(Unit)
+
+ val updatedConfig = config.toggle() // After successful switch, toggle the state
+ coEvery { preferencesRepository.setToggleModeConfig(updatedConfig) } returns Unit
+
+ // When
+ val result = useCase(testSubId)
+
+ // Then
+ assertTrue(result.isSuccess)
+ assertEquals(NetworkMode.LTE_ONLY, result.getOrNull())
+ coVerify { networkControlRepository.setNetworkMode(testSubId, NetworkMode.LTE_ONLY) }
+ coVerify { preferencesRepository.setToggleModeConfig(updatedConfig) }
+ }
+
+ @Test
+ fun `should handle alternating sequence correctly`() = runTest {
+ // Given
+ val initialConfig = ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY, nextModeIsB = true)
+ coEvery { preferencesRepository.getToggleModeConfig() } returns initialConfig
+
+ // First call should switch to NR_ONLY
+ coEvery { networkControlRepository.setNetworkMode(testSubId, NetworkMode.NR_ONLY) } returns Result.success(Unit)
+ val afterFirstToggle = initialConfig.toggle()
+ coEvery { preferencesRepository.setToggleModeConfig(afterFirstToggle) } returns Unit
+
+ // When
+ val firstResult = useCase(testSubId)
+
+ // Then
+ assertTrue(firstResult.isSuccess)
+ assertEquals(NetworkMode.NR_ONLY, firstResult.getOrNull())
+
+ // The next call should switch to LTE_ONLY
+ coEvery { preferencesRepository.getToggleModeConfig() } returns afterFirstToggle
+ coEvery { networkControlRepository.setNetworkMode(testSubId, NetworkMode.LTE_ONLY) } returns Result.success(Unit)
+ val afterSecondToggle = afterFirstToggle.toggle()
+ coEvery { preferencesRepository.setToggleModeConfig(afterSecondToggle) } returns Unit
+
+ val secondResult = useCase(testSubId)
+
+ assertTrue(secondResult.isSuccess)
+ assertEquals(NetworkMode.LTE_ONLY, secondResult.getOrNull())
+ }
+
+ @Test
+ fun `should return failure when setNetworkMode fails and not update config`() = runTest {
+ // Given
+ val config = ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY, nextModeIsB = true)
+ coEvery { preferencesRepository.getToggleModeConfig() } returns config
+
+ val exception = RuntimeException("Network mode not supported")
+ coEvery { networkControlRepository.setNetworkMode(testSubId, NetworkMode.NR_ONLY) } returns Result.failure(exception)
+
+ // When
+ val result = useCase(testSubId)
+
+ // Then
+ assertTrue(result.isFailure)
+ assertEquals(exception, result.exceptionOrNull())
+ // Config should not be updated when network mode change fails
+ coVerify(exactly = 0) { preferencesRepository.setToggleModeConfig(any()) }
+ }
+
+ @Test
+ fun `should handle repository exception`() = runTest {
+ // Given
+ coEvery { preferencesRepository.getToggleModeConfig() } throws RuntimeException("Repository error")
+
+ // When
+ val result = useCase(testSubId)
+
+ // Then
+ assertTrue(result.isFailure)
+ assertTrue(result.exceptionOrNull() is RuntimeException)
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/supernova/networkswitch/presentation/viewmodel/MainViewModelTest.kt b/app/src/test/java/com/supernova/networkswitch/presentation/viewmodel/MainViewModelTest.kt
deleted file mode 100644
index 7ba1e2a..0000000
--- a/app/src/test/java/com/supernova/networkswitch/presentation/viewmodel/MainViewModelTest.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-package com.supernova.networkswitch.presentation.viewmodel
-
-import com.supernova.networkswitch.domain.model.CompatibilityState
-import com.supernova.networkswitch.domain.model.ControlMethod
-import com.supernova.networkswitch.domain.repository.PreferencesRepository
-import com.supernova.networkswitch.domain.usecase.*
-import com.supernova.networkswitch.util.CoroutineTestRule
-import io.mockk.coEvery
-import io.mockk.coVerify
-import io.mockk.mockk
-import android.telephony.SubscriptionManager
-import io.mockk.every
-import io.mockk.mockkStatic
-import io.mockk.unmockkAll
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-
-@ExperimentalCoroutinesApi
-class MainViewModelTest {
-
- @get:Rule
- val coroutineTestRule = CoroutineTestRule()
-
- private lateinit var checkCompatibilityUseCase: CheckCompatibilityUseCase
- private lateinit var getNetworkStateUseCase: GetNetworkStateUseCase
- private lateinit var toggleNetworkModeUseCase: ToggleNetworkModeUseCase
- private lateinit var updateControlMethodUseCase: UpdateControlMethodUseCase
- private lateinit var preferencesRepository: PreferencesRepository
-
- private lateinit var viewModel: MainViewModel
-
- @Before
- fun setUp() {
- checkCompatibilityUseCase = mockk()
- getNetworkStateUseCase = mockk()
- toggleNetworkModeUseCase = mockk()
- updateControlMethodUseCase = mockk()
- preferencesRepository = mockk()
-
- mockkStatic(SubscriptionManager::class)
- every { SubscriptionManager.getDefaultDataSubscriptionId() } returns 1
-
- coEvery { preferencesRepository.observeControlMethod() } returns flowOf(ControlMethod.SHIZUKU)
- coEvery { checkCompatibilityUseCase() } returns CompatibilityState.Pending
- coEvery { getNetworkStateUseCase(any()) } returns Result.success(false)
- coEvery { updateControlMethodUseCase(any()) } returns Unit
- }
-
- @After
- fun tearDown() {
- unmockkAll()
- }
-
- private fun createViewModel() {
- viewModel = MainViewModel(
- checkCompatibilityUseCase,
- getNetworkStateUseCase,
- toggleNetworkModeUseCase,
- updateControlMethodUseCase,
- preferencesRepository
- )
- }
-
- @Test
- fun `init calls required methods`() = runTest {
- createViewModel()
- coVerify { preferencesRepository.observeControlMethod() }
- coVerify { checkCompatibilityUseCase() }
- coVerify { getNetworkStateUseCase(any()) }
- }
-
- @Test
- fun `toggleNetworkMode success updates networkState`() = runTest {
- coEvery { toggleNetworkModeUseCase(any()) } returns Result.success(true)
- createViewModel()
-
- viewModel.toggleNetworkMode()
-
- assertTrue(viewModel.networkState)
- assertFalse(viewModel.isLoading)
- }
-
- @Test
- fun `toggleNetworkMode failure refreshes network state`() = runTest {
- coEvery { toggleNetworkModeUseCase(any()) } returns Result.failure(Exception())
- coEvery { getNetworkStateUseCase(any()) } returns Result.success(false)
- createViewModel()
-
- viewModel.toggleNetworkMode()
-
- assertFalse(viewModel.networkState)
- assertFalse(viewModel.isLoading)
- coVerify(exactly = 2) { getNetworkStateUseCase(any()) } // Initial + refresh
- }
-
- @Test
- fun `retryCompatibilityCheck calls use case`() = runTest {
- createViewModel()
- viewModel.retryCompatibilityCheck()
- coVerify(exactly = 2) { checkCompatibilityUseCase() } // Initial + retry
- }
-
- @Test
- fun `refreshAllData calls required methods`() = runTest {
- createViewModel()
- viewModel.refreshAllData()
- coVerify(exactly = 2) { checkCompatibilityUseCase() } // Initial + refresh
- coVerify(exactly = 2) { getNetworkStateUseCase(any()) } // Initial + refresh
- }
-
- @Test
- fun `switchToMethod calls use case`() = runTest {
- createViewModel()
- viewModel.switchToMethod(ControlMethod.ROOT)
- coVerify { updateControlMethodUseCase(ControlMethod.ROOT) }
- }
-
- @Test
- fun `compatibilityState is updated on check`() = runTest {
- val expectedState = CompatibilityState.Compatible
- coEvery { checkCompatibilityUseCase() } returns expectedState
-
- createViewModel()
-
- assertEquals(expectedState, viewModel.compatibilityState)
- }
-}
diff --git a/fastlane/metadata/android/en-US/changelogs/default.txt b/fastlane/metadata/android/en-US/changelogs/default.txt
new file mode 100644
index 0000000..1846a34
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/default.txt
@@ -0,0 +1,6 @@
+• Network mode control supporting 34 different configurations
+• Configurable toggle between any two network modes
+• Support for 2G/3G/4G/5G and regional network combinations
+• Smart Quick Settings tile showing current and next modes
+• Network mode configuration screen with preview
+• Root and Shizuku dual control methods for maximum compatibility
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index 7a1aa6b..ca644c1 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -1 +1 @@
-Network Switch enables you to toggle between 4G and 5G network modes with dual control methods: Root access for rooted devices and Shizuku for non-rooted devices.
Purpose
Network Switch allows Android users to manually control their device's network preference between pure 4G (LTE-only) and 5G (NR-only) modes. This is useful for:
- Quick network switching through Quick Settings tile
- Optimizing battery life by forcing 4G mode
- Maximizing speed by forcing 5G mode where available
- Managing data usage and connectivity preferences
The app provides two methods of operation:
- Root Method: Direct system access for rooted devices
- Shizuku Method: System access through Shizuku service for non-rooted devices
Features
- Pure network mode switching (LTE-only for 4G, NR-only for 5G)
- Quick Settings tile for instant access
- Dual control methods (Root and Shizuku)
- Modern Material Design 3 interface
- Clean architecture implementation
- Privacy-focused (no internet permissions)
- Intelligent device compatibility detection
- Automatic fallback for unsupported devices
\ No newline at end of file
+Network Switch provides control over 34 network modes including 2G, 3G, 4G, and 5G configurations. Works with both Root and Shizuku permissions.
Features
- 34 network modes: Basic (2G/3G/4G/5G), Combined, Regional (CDMA, TD-SCDMA), Global
- Configurable toggle between any two modes
- Quick Settings tile with current/next mode display
- Battery and speed optimization through mode selection
- Modern Material Design 3 interface
- Privacy-focused (no internet permissions)
- Compatible with rooted (Root) and non-rooted (Shizuku) devices
Use Cases
Optimize battery life, maximize speed, improve coverage in poor signal areas, manage data usage, and test network compatibility.
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt
index 561c3c7..83952a0 100644
--- a/fastlane/metadata/android/en-US/short_description.txt
+++ b/fastlane/metadata/android/en-US/short_description.txt
@@ -1 +1 @@
-One-tap network switching for Android
\ No newline at end of file
+One-tap network switching.
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt
new file mode 100644
index 0000000..86ab1b3
--- /dev/null
+++ b/fastlane/metadata/android/en-US/title.txt
@@ -0,0 +1 @@
+Network Switch - One-tap network switching
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 987146c..a1b1ffd 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -32,6 +32,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" }
androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }