diff --git a/README.md b/README.md index 9d3b31d..f663f99 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,36 @@

Network Switch

-

Modern Android app for 4G/5G network mode switching

+

Modern Android app for network mode switching

[![Build Status](https://img.shields.io/github/actions/workflow/status/aunchagaonkar/NetworkSwitch/build.yml)](https://github.com/aunchagaonkar/NetworkSwitch/actions) [![License](https://img.shields.io/github/license/aunchagaonkar/NetworkSwitch)](https://github.com/aunchagaonkar/NetworkSwitch/blob/main/LICENSE) [![Downloads](https://img.shields.io/github/downloads/aunchagaonkar/NetworkSwitch/total)](https://github.com/aunchagaonkar/NetworkSwitch/releases) [![Release](https://img.shields.io/github/v/release/aunchagaonkar/NetworkSwitch)](https://github.com/aunchagaonkar/NetworkSwitch/releases/latest) +[![Awesome](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/timschneeb/awesome-shizuku)
--- -A modern Android application that enables users to toggle between 4G and 5G network modes with dual control methods: Root access for rooted devices and Shizuku for non-rooted devices. Built using Jetpack Compose and Material Design 3. +A modern Android application that enables users to toggle between network modes, supporting 34 different network configurations including 2G, 3G, 4G, and 5G combinations. Features dual control methods: Root access for rooted devices and Shizuku for non-rooted devices. Built using Jetpack Compose and Material Design 3. ## 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: +Network Switch provides control over your device's network modes, supporting 34 different configurations from basic 2G-only to advanced 5G combinations. This includes: -- 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 +- **Basic Modes**: GSM (2G), WCDMA (3G), LTE (4G), NR (5G) only +- **Combined Modes**: Various 2G/3G/4G/5G combinations +- **Regional Modes**: CDMA support for US carriers, TD-SCDMA for Chinese networks +- **Global Modes**: Network support for international usage + +### Use Cases +- Quick network switching through customizable Quick Settings tile +- Battery optimization by selecting power-efficient modes +- Speed optimization by forcing high-performance modes +- Coverage optimization in areas with poor signal quality +- Data usage management with specific network restrictions +- Testing network compatibility and performance The app provides two methods of operation: - **Root Method**: Direct system access for rooted devices @@ -29,14 +38,41 @@ The app provides two methods of operation: ## 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 +- **Network Control**: 34 different network modes including pure and combined configurations +- **Privacy-Focused**: No internet permissions, all operations are local +- **Configurable Toggle Modes**: Choose any two network modes for quick switching +- **Quick Settings Tile**: Shows current and next modes with customizable configuration +- **Dual Control Methods**: Root and Shizuku support for compatibility +- **Modern Material Design 3 Interface**: Clean, intuitive UI with extensive configuration options + +## Configuration + +### Network Mode Selection +The app supports 34 different network modes: + +**Basic Modes:** +- GSM Only (2G) +- WCDMA Only (3G) +- LTE Only (4G) +- NR Only (5G) + +**Combined Modes:** +- 2G/3G combinations with preference settings +- 3G/4G combinations (LTE/WCDMA) +- 4G/5G combinations (NR/LTE) +- Multi-generation support (2G/3G/4G/5G) + +**Regional Modes:** +- CDMA support for US carriers (Verizon) +- TD-SCDMA support for Chinese networks +- Global modes for international roaming + +### Toggle Configuration +1. Open the app and tap the configuration icon +2. Select **Mode A** and **Mode B** from the dropdown menus +3. Preview your configuration +4. Save the configuration +5. Use the main toggle or Quick Settings tile to switch between modes ## Screenshots @@ -62,7 +98,7 @@ The app provides two methods of operation: [Get it on GitHub](https://github.com/aunchagaonkar/NetworkSwitch/releases) [Get it at IzzyOnDroid](https://apt.izzysoft.de/packages/com.supernova.networkswitch) [Get it on Obtainium](https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://github.com/aunchagaonkar/NetworkSwitch/) - +[](https://www.openapk.net/network-switch/com.supernova.networkswitch/) 1. Download the latest APK from the Releases page @@ -70,7 +106,8 @@ The app provides two methods of operation: 3. Choose your preferred control method: - **Root**: Grant root permissions when prompted - **Shizuku**: Install Shizuku app and grant permission -4. Add the "4G/5G Toggle" tile to Quick Settings +4. Configure your preferred network modes in the app +5. Add the "Network Switch" tile to Quick Settings for instant access ## Project Structure @@ -78,25 +115,29 @@ The app provides two methods of operation: app/ ├── src/main/java/com/supernova/networkswitch/ │ ├── presentation/ # UI Layer -│ │ ├── ui/ # Compose UI components -│ │ ├── viewmodel/ # ViewModels -│ │ └── theme/ # Material Design theme +│ │ ├── ui/ +│ │ │ ├── activity/ # Activities +│ │ │ ├── composable/ # Reusable Compose components +│ │ │ └── components/ # Shared UI components +│ │ ├── viewmodel/ # ViewModels with state management +│ │ └── theme/ # Material Design 3 theme │ ├── domain/ # Business Logic Layer │ │ ├── model/ # Domain models │ │ ├── repository/ # Repository interfaces │ │ └── usecase/ # Business use cases │ ├── data/ # Data Layer │ │ ├── repository/ # Repository implementations -│ │ ├── source/ # Data sources (Root, Shizuku) -│ │ └── preferences/ # Settings storage +│ │ └── source/ # Data sources │ ├── service/ # System Services -│ │ ├── tile/ # Quick Settings tile -│ │ └── network/ # Network management -│ └── di/ # Dependency Injection modules +│ │ ├── NetworkTileService # Smart Quick Settings tile +│ │ ├── RootNetworkControllerService # Root-based network control +│ │ └── ShizukuControllerService # Shizuku-based network control +│ └── di/ # Hilt dependency injection modules +├── src/main/aidl/ # AIDL interfaces for IPC ├── build.gradle.kts # App build configuration └── proguard-rules.pro # ProGuard rules -hiddenapi/ # Android Hidden API access +hiddenapi/ # Android Hidden API access module ├── src/main/aidl/ # AIDL interfaces └── build.gradle.kts # Hidden API build config @@ -129,13 +170,12 @@ cd NetworkSwitch ## TODO - [x] Add unit tests for all core components -- [ ] Add network speed monitoring -- [ ] Implement network statistics tracking -- [ ] Add support for 3G fallback modes +- [x] Add customizable network mode support (34 modes) +- [ ] Implement adaptive app icon, with icon-pack launcher compatibility +- [ ] Implement scheduled/automatic network switching +- [ ] Add network speed monitoring and performance metrics - [ ] Add multi-language support -- [ ] Implement network signal strength indicators -- [ ] Add scheduled network switching -- [ ] Add network performance benchmarking +- [ ] Add network performance benchmarking tools ## Contributing @@ -166,10 +206,14 @@ To run the unit tests, execute the following command from the root of the projec ./gradlew test ``` +**Testing Guidelines:** - Add unit tests for new functionality - Test on both rooted and non-rooted devices -- Verify compatibility across different Android versions -- Test network switching functionality thoroughly +- Verify compatibility across different Android versions (10+) +- Test network mode switching with various configurations +- Validate AIDL interface implementations +- Test Quick Settings tile functionality +- Verify configuration persistence and restoration ### Reporting Issues - Use the GitHub issue tracker diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4098045..1b3132a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,13 +23,7 @@ android { useSupportLibrary = true } - // Resource optimization resourceConfigurations += listOf("en", "xxhdpi") - - // Disable unnecessary features - vectorDrawables { - useSupportLibrary = true - } } buildTypes { @@ -42,8 +36,6 @@ android { ) signingConfig = signingConfigs.getByName("debug") isDebuggable = false - - // Additional optimizations isJniDebuggable = false isPseudoLocalesEnabled = false } @@ -94,31 +86,12 @@ android { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/*.kotlin_module" - excludes += "**/*.txt" - excludes += "**/*.properties" - excludes += "/META-INF/DEPENDENCIES" - excludes += "/META-INF/LICENSE" - excludes += "/META-INF/LICENSE.txt" - excludes += "/META-INF/license.txt" - excludes += "/META-INF/NOTICE" - excludes += "/META-INF/NOTICE.txt" - excludes += "/META-INF/notice.txt" - excludes += "/META-INF/ASL2.0" excludes += "/META-INF/maven/**" - excludes += "/META-INF/proguard/**" - excludes += "/META-INF/versions/**" - excludes += "**/*.kotlin_metadata" - excludes += "kotlin/**" - excludes += "META-INF/com.android.tools/**" excludes += "/androidsupportmultidexversion.txt" } - // JNI libraries optimization jniLibs { useLegacyPackaging = false - // Exclude native libraries from packaging - excludes += "**/libandroidx.graphics.path.so" - excludes += "**/libdatastore_shared_counter.so" } } @@ -156,10 +129,8 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) - debugImplementation(libs.androidx.ui.tooling.preview) // Move to debug only implementation(libs.androidx.material3) - implementation("androidx.compose.runtime:runtime") - implementation("androidx.compose.material:material-icons-extended") + implementation(libs.androidx.material.icons.extended) implementation(libs.androidx.lifecycle.viewmodel.compose) // Dependency Injection diff --git a/app/src/androidTest/java/com/supernova/networkswitch/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/supernova/networkswitch/ExampleInstrumentedTest.kt index 7f5aebf..824a901 100644 --- a/app/src/androidTest/java/com/supernova/networkswitch/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/supernova/networkswitch/ExampleInstrumentedTest.kt @@ -9,9 +9,6 @@ import org.junit.runner.RunWith import org.junit.Assert.* import org.junit.Rule -/** - * Instrumented tests for the Network Switch application. - */ @RunWith(AndroidJUnit4::class) class NetworkSwitchInstrumentedTest { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5ebbeb8..3552c09 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,13 @@ android:parentActivityName=".presentation.ui.activity.MainActivity" android:theme="@style/Theme.NetworkSwitch" /> + + { + override suspend fun setNetworkMode(subId: Int, mode: NetworkMode): Result { return try { val method = preferencesRepository.getControlMethod() val dataSource = getDataSource(method) - dataSource.setNetworkState(subId, enabled) - _connectionState.value = dataSource.isConnected() + dataSource.setNetworkMode(subId, mode) Result.success(Unit) } catch (e: Exception) { Result.failure(e) } } - override fun observeConnectionState(): Flow = _connectionState.asStateFlow() - /** * Reset connections for both data sources - useful when switching control methods */ override suspend fun resetConnections() { rootDataSource.resetConnection() shizukuDataSource.resetConnection() - _connectionState.value = false } private fun getDataSource(method: ControlMethod): NetworkControlDataSource { diff --git a/app/src/main/java/com/supernova/networkswitch/data/repository/PreferencesRepositoryImpl.kt b/app/src/main/java/com/supernova/networkswitch/data/repository/PreferencesRepositoryImpl.kt index 96644ad..7f84c4d 100644 --- a/app/src/main/java/com/supernova/networkswitch/data/repository/PreferencesRepositoryImpl.kt +++ b/app/src/main/java/com/supernova/networkswitch/data/repository/PreferencesRepositoryImpl.kt @@ -2,6 +2,7 @@ package com.supernova.networkswitch.data.repository import com.supernova.networkswitch.data.source.PreferencesDataSource import com.supernova.networkswitch.domain.model.ControlMethod +import com.supernova.networkswitch.domain.model.ToggleModeConfig import com.supernova.networkswitch.domain.repository.PreferencesRepository import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -26,4 +27,16 @@ class PreferencesRepositoryImpl @Inject constructor( override fun observeControlMethod(): Flow { return preferencesDataSource.observeControlMethod() } + + override suspend fun getToggleModeConfig(): ToggleModeConfig { + return preferencesDataSource.getToggleModeConfig() + } + + override suspend fun setToggleModeConfig(config: ToggleModeConfig) { + preferencesDataSource.setToggleModeConfig(config) + } + + override fun observeToggleModeConfig(): Flow { + return preferencesDataSource.observeToggleModeConfig() + } } diff --git a/app/src/main/java/com/supernova/networkswitch/data/source/NetworkControlDataSource.kt b/app/src/main/java/com/supernova/networkswitch/data/source/NetworkControlDataSource.kt index 17478b4..d6fa53c 100644 --- a/app/src/main/java/com/supernova/networkswitch/data/source/NetworkControlDataSource.kt +++ b/app/src/main/java/com/supernova/networkswitch/data/source/NetworkControlDataSource.kt @@ -1,13 +1,12 @@ package com.supernova.networkswitch.data.source import com.supernova.networkswitch.domain.model.CompatibilityState +import com.supernova.networkswitch.domain.model.NetworkMode -/** - * Interface for network control data sources - */ interface NetworkControlDataSource { suspend fun checkCompatibility(subId: Int): CompatibilityState - suspend fun getNetworkState(subId: Int): Boolean - suspend fun setNetworkState(subId: Int, enabled: Boolean) + suspend fun getCurrentNetworkMode(subId: Int): NetworkMode? + suspend fun setNetworkMode(subId: Int, mode: NetworkMode) fun isConnected(): Boolean + fun resetConnection() } diff --git a/app/src/main/java/com/supernova/networkswitch/data/source/PreferencesDataSource.kt b/app/src/main/java/com/supernova/networkswitch/data/source/PreferencesDataSource.kt index 4cc910e..e8d8f02 100644 --- a/app/src/main/java/com/supernova/networkswitch/data/source/PreferencesDataSource.kt +++ b/app/src/main/java/com/supernova/networkswitch/data/source/PreferencesDataSource.kt @@ -4,17 +4,17 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.booleanPreferencesKey import com.supernova.networkswitch.domain.model.ControlMethod +import com.supernova.networkswitch.domain.model.NetworkMode +import com.supernova.networkswitch.domain.model.ToggleModeConfig import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import javax.inject.Inject import javax.inject.Singleton -/** - * DataStore-based preferences data source - * Modern replacement for SharedPreferences-based SettingsManager - */ @Singleton class PreferencesDataSource @Inject constructor( private val dataStore: DataStore @@ -22,43 +22,74 @@ class PreferencesDataSource @Inject constructor( companion object { private val CONTROL_METHOD_KEY = stringPreferencesKey("control_method") + private val TOGGLE_MODE_A_KEY = intPreferencesKey("toggle_mode_a") + private val TOGGLE_MODE_B_KEY = intPreferencesKey("toggle_mode_b") + private val TOGGLE_NEXT_IS_B_KEY = booleanPreferencesKey("toggle_next_is_b") + private const val DEFAULT_CONTROL_METHOD = "SHIZUKU" + + private val DEFAULT_MODE_A = NetworkMode.LTE_ONLY + private val DEFAULT_MODE_B = NetworkMode.NR_ONLY + private const val DEFAULT_NEXT_IS_B = true + } + + private fun parseControlMethod(methodString: String?): ControlMethod { + return try { + ControlMethod.valueOf(methodString ?: DEFAULT_CONTROL_METHOD) + } catch (e: IllegalArgumentException) { + ControlMethod.SHIZUKU + } } - /** - * Get preferred control method - */ suspend fun getControlMethod(): ControlMethod { return dataStore.data.map { preferences -> - val methodString = preferences[CONTROL_METHOD_KEY] ?: DEFAULT_CONTROL_METHOD - try { - ControlMethod.valueOf(methodString) - } catch (e: IllegalArgumentException) { - ControlMethod.SHIZUKU // Fallback to SHIZUKU if invalid value - } - }.first() // Use first() to get the current value without hanging + parseControlMethod(preferences[CONTROL_METHOD_KEY]) + }.first() } - /** - * Set preferred control method - */ suspend fun setControlMethod(method: ControlMethod) { dataStore.edit { preferences -> preferences[CONTROL_METHOD_KEY] = method.name } } - /** - * Observe control method changes - */ fun observeControlMethod(): Flow { return dataStore.data.map { preferences -> - val methodString = preferences[CONTROL_METHOD_KEY] ?: DEFAULT_CONTROL_METHOD - try { - ControlMethod.valueOf(methodString) - } catch (e: IllegalArgumentException) { - ControlMethod.SHIZUKU // Fallback to SHIZUKU if invalid value - } + parseControlMethod(preferences[CONTROL_METHOD_KEY]) + } + } + + suspend fun getToggleModeConfig(): ToggleModeConfig { + return dataStore.data.map { preferences -> + val modeAValue = preferences[TOGGLE_MODE_A_KEY] ?: DEFAULT_MODE_A.value + val modeBValue = preferences[TOGGLE_MODE_B_KEY] ?: DEFAULT_MODE_B.value + val nextIsB = preferences[TOGGLE_NEXT_IS_B_KEY] ?: DEFAULT_NEXT_IS_B + + val modeA = NetworkMode.fromValue(modeAValue) ?: DEFAULT_MODE_A + val modeB = NetworkMode.fromValue(modeBValue) ?: DEFAULT_MODE_B + + ToggleModeConfig(modeA, modeB, nextIsB) + }.first() + } + + suspend fun setToggleModeConfig(config: ToggleModeConfig) { + dataStore.edit { preferences -> + preferences[TOGGLE_MODE_A_KEY] = config.modeA.value + preferences[TOGGLE_MODE_B_KEY] = config.modeB.value + preferences[TOGGLE_NEXT_IS_B_KEY] = config.nextModeIsB + } + } + + fun observeToggleModeConfig(): Flow { + return dataStore.data.map { preferences -> + val modeAValue = preferences[TOGGLE_MODE_A_KEY] ?: DEFAULT_MODE_A.value + val modeBValue = preferences[TOGGLE_MODE_B_KEY] ?: DEFAULT_MODE_B.value + val nextIsB = preferences[TOGGLE_NEXT_IS_B_KEY] ?: DEFAULT_NEXT_IS_B + + val modeA = NetworkMode.fromValue(modeAValue) ?: DEFAULT_MODE_A + val modeB = NetworkMode.fromValue(modeBValue) ?: DEFAULT_MODE_B + + ToggleModeConfig(modeA, modeB, nextIsB) } } } diff --git a/app/src/main/java/com/supernova/networkswitch/data/source/RootNetworkControlDataSource.kt b/app/src/main/java/com/supernova/networkswitch/data/source/RootNetworkControlDataSource.kt index 469a6e8..26b1748 100644 --- a/app/src/main/java/com/supernova/networkswitch/data/source/RootNetworkControlDataSource.kt +++ b/app/src/main/java/com/supernova/networkswitch/data/source/RootNetworkControlDataSource.kt @@ -8,6 +8,7 @@ import android.os.IBinder import com.supernova.networkswitch.IRootController import com.supernova.networkswitch.service.RootNetworkControllerService import com.supernova.networkswitch.domain.model.CompatibilityState +import com.supernova.networkswitch.domain.model.NetworkMode import com.supernova.networkswitch.util.Utils import com.topjohnwu.superuser.ipc.RootService import kotlinx.coroutines.suspendCancellableCoroutine @@ -17,9 +18,6 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import dagger.hilt.android.qualifiers.ApplicationContext -/** - * Root-based network control data source - */ @Singleton class RootNetworkControlDataSource @Inject constructor( @ApplicationContext private val context: Context @@ -36,22 +34,24 @@ class RootNetworkControlDataSource @Inject constructor( } } - override suspend fun getNetworkState(subId: Int): Boolean { - val controller = getNetworkController() - return controller?.getNetworkState(subId) ?: false + override suspend fun getCurrentNetworkMode(subId: Int): NetworkMode? { + return try { + val controller = getNetworkController() + val modeValue = controller?.getCurrentNetworkMode(subId) ?: -1 + if (modeValue == -1) null else NetworkMode.fromValue(modeValue) + } catch (e: Exception) { + null + } } - override suspend fun setNetworkState(subId: Int, enabled: Boolean) { + override suspend fun setNetworkMode(subId: Int, mode: NetworkMode) { val controller = getNetworkController() - controller?.setNetworkState(subId, enabled) + controller?.setNetworkMode(subId, mode.value) } override fun isConnected(): Boolean = isServiceConnected - /** - * Reset connection state - useful when switching control methods - */ - fun resetConnection() { + override fun resetConnection() { networkController = null isServiceConnected = false } diff --git a/app/src/main/java/com/supernova/networkswitch/data/source/ShizukuNetworkControlDataSource.kt b/app/src/main/java/com/supernova/networkswitch/data/source/ShizukuNetworkControlDataSource.kt index e10c651..90e7e8e 100644 --- a/app/src/main/java/com/supernova/networkswitch/data/source/ShizukuNetworkControlDataSource.kt +++ b/app/src/main/java/com/supernova/networkswitch/data/source/ShizukuNetworkControlDataSource.kt @@ -7,6 +7,7 @@ import android.content.pm.PackageManager import android.os.IBinder import com.supernova.networkswitch.IShizukuController import com.supernova.networkswitch.domain.model.CompatibilityState +import com.supernova.networkswitch.domain.model.NetworkMode import com.supernova.networkswitch.service.ShizukuControllerService import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -18,9 +19,6 @@ import kotlin.coroutines.resume import com.supernova.networkswitch.BuildConfig import dagger.hilt.android.qualifiers.ApplicationContext -/** - * Shizuku-based network control data source - */ @Singleton class ShizukuNetworkControlDataSource @Inject constructor( @ApplicationContext private val context: Context @@ -53,22 +51,23 @@ class ShizukuNetworkControlDataSource @Inject constructor( } } - override suspend fun getNetworkState(subId: Int): Boolean { + override suspend fun getCurrentNetworkMode(subId: Int): NetworkMode? { return if (ensureServiceBinding()) { try { - userService?.getNetworkState(subId) ?: false + val modeValue = userService?.getCurrentNetworkMode(subId) ?: -1 + if (modeValue == -1) null else NetworkMode.fromValue(modeValue) } catch (e: Exception) { - false + null } } else { - false + null } } - override suspend fun setNetworkState(subId: Int, enabled: Boolean) { + override suspend fun setNetworkMode(subId: Int, mode: NetworkMode) { if (ensureServiceBinding()) { try { - userService?.setNetworkState(subId, enabled) + userService?.setNetworkMode(subId, mode.value) } catch (e: Exception) { throw e } @@ -79,10 +78,7 @@ class ShizukuNetworkControlDataSource @Inject constructor( override fun isConnected(): Boolean = _isConnected.value - /** - * Reset connection state - useful when switching control methods - */ - fun resetConnection() { + override fun resetConnection() { userService = null _isConnected.value = false } diff --git a/app/src/main/java/com/supernova/networkswitch/domain/model/NetworkSwitchModels.kt b/app/src/main/java/com/supernova/networkswitch/domain/model/NetworkSwitchModels.kt index 42fba74..79b2661 100644 --- a/app/src/main/java/com/supernova/networkswitch/domain/model/NetworkSwitchModels.kt +++ b/app/src/main/java/com/supernova/networkswitch/domain/model/NetworkSwitchModels.kt @@ -1,33 +1,88 @@ package com.supernova.networkswitch.domain.model -/** - * Enumeration of control methods for network switching - */ enum class ControlMethod { ROOT, SHIZUKU } -/** - * Network mode types - */ -enum class NetworkMode { - FOUR_G_ONLY, - FIVE_G_ONLY +enum class NetworkMode(val displayName: String, val value: Int) { + // Basic modes + GSM_ONLY("2G Only (GSM)", 1), + WCDMA_ONLY("3G Only (WCDMA)", 2), + LTE_ONLY("4G Only (LTE)", 11), + NR_ONLY("5G Only (NR)", 23), + + // Preferred modes + WCDMA_PREF("2G/3G (3G Preferred)", 0), + GSM_UMTS("2G/3G (Auto)", 3), + + // Combined modes + LTE_GSM_WCDMA("2G/3G/4G (LTE/GSM/WCDMA)", 9), + LTE_WCDMA("3G/4G (LTE/WCDMA)", 12), + NR_LTE("4G/5G (NR/LTE)", 24), + NR_LTE_GSM_WCDMA("2G/3G/4G/5G (NR/LTE/GSM/WCDMA)", 26), + NR_LTE_WCDMA("3G/4G/5G (NR/LTE/WCDMA)", 28), + + // CDMA modes for US carriers + CDMA("CDMA (Auto)", 4), + CDMA_NO_EVDO("CDMA Only", 5), + EVDO_NO_CDMA("EvDo Only", 6), + LTE_CDMA_EVDO("CDMA/4G (LTE/CDMA/EvDo)", 8), + NR_LTE_CDMA_EVDO("CDMA/4G/5G (NR/LTE/CDMA/EvDo)", 25), + + // Global modes + GLOBAL("Global (All)", 7), + LTE_CDMA_EVDO_GSM_WCDMA("Global 4G (LTE/CDMA/EvDo/GSM/WCDMA)", 10), + NR_LTE_CDMA_EVDO_GSM_WCDMA("Global 5G (NR/LTE/CDMA/EvDo/GSM/WCDMA)", 27), + + // TD-SCDMA modes for China + TDSCDMA_ONLY("TD-SCDMA Only", 13), + TDSCDMA_WCDMA("3G (TD-SCDMA/WCDMA)", 14), + LTE_TDSCDMA("4G (LTE/TD-SCDMA)", 15), + TDSCDMA_GSM("2G/TD-SCDMA", 16), + LTE_TDSCDMA_GSM("2G/4G (LTE/TD-SCDMA/GSM)", 17), + TDSCDMA_GSM_WCDMA("2G/3G (TD-SCDMA/GSM/WCDMA)", 18), + LTE_TDSCDMA_WCDMA("3G/4G (LTE/TD-SCDMA/WCDMA)", 19), + LTE_TDSCDMA_GSM_WCDMA("2G/3G/4G (LTE/TD-SCDMA/GSM/WCDMA)", 20), + TDSCDMA_CDMA_EVDO_GSM_WCDMA("Global 3G (TD-SCDMA/CDMA/EvDo/GSM/WCDMA)", 21), + LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA("Global 4G (LTE/TD-SCDMA/CDMA/EvDo/GSM/WCDMA)", 22), + NR_LTE_TDSCDMA("4G/5G (NR/LTE/TD-SCDMA)", 29), + NR_LTE_TDSCDMA_GSM("2G/4G/5G (NR/LTE/TD-SCDMA/GSM)", 30), + NR_LTE_TDSCDMA_WCDMA("3G/4G/5G (NR/LTE/TD-SCDMA/WCDMA)", 31), + NR_LTE_TDSCDMA_GSM_WCDMA("2G/3G/4G/5G (NR/LTE/TD-SCDMA/GSM/WCDMA)", 32), + NR_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA("Global 5G + TD-SCDMA (All Networks)", 33); + + companion object { + /** + * Get NetworkMode by RIL constant value + */ + fun fromValue(value: Int): NetworkMode? { + return values().find { it.value == value } + } + } } /** - * Network switch configuration + * Configuration for the two modes that can be toggled between */ -data class NetworkSwitchConfig( - val controlMethod: ControlMethod, - val isEnabled: Boolean, - val currentMode: NetworkMode -) +data class ToggleModeConfig( + val modeA: NetworkMode, + val modeB: NetworkMode, + val nextModeIsB: Boolean = true +) { + fun getNextMode(): NetworkMode { + return if (nextModeIsB) modeB else modeA + } + + fun getCurrentMode(): NetworkMode { + return if (nextModeIsB) modeA else modeB + } + + fun toggle(): ToggleModeConfig { + return copy(nextModeIsB = !nextModeIsB) + } +} -/** - * Compatibility state for network switching - */ sealed class CompatibilityState { object Pending : CompatibilityState() object Compatible : CompatibilityState() diff --git a/app/src/main/java/com/supernova/networkswitch/domain/repository/Repositories.kt b/app/src/main/java/com/supernova/networkswitch/domain/repository/Repositories.kt index 4e31c83..047b601 100644 --- a/app/src/main/java/com/supernova/networkswitch/domain/repository/Repositories.kt +++ b/app/src/main/java/com/supernova/networkswitch/domain/repository/Repositories.kt @@ -2,6 +2,8 @@ package com.supernova.networkswitch.domain.repository import com.supernova.networkswitch.domain.model.CompatibilityState import com.supernova.networkswitch.domain.model.ControlMethod +import com.supernova.networkswitch.domain.model.NetworkMode +import com.supernova.networkswitch.domain.model.ToggleModeConfig import kotlinx.coroutines.flow.Flow /** @@ -14,19 +16,14 @@ interface NetworkControlRepository { suspend fun checkCompatibility(method: ControlMethod): CompatibilityState /** - * Get current 5G enabled state + * Get current network mode */ - suspend fun getNetworkState(subId: Int): Boolean + suspend fun getCurrentNetworkMode(subId: Int): NetworkMode? /** - * Set 5G enabled state + * Set network mode */ - suspend fun setNetworkState(subId: Int, enabled: Boolean): Result - - /** - * Observe connection state changes - */ - fun observeConnectionState(): Flow + suspend fun setNetworkMode(subId: Int, mode: NetworkMode): Result /** * Reset connections - useful when switching control methods @@ -52,4 +49,19 @@ interface PreferencesRepository { * Observe control method changes */ fun observeControlMethod(): Flow + + /** + * Get toggle mode configuration + */ + suspend fun getToggleModeConfig(): ToggleModeConfig + + /** + * Set toggle mode configuration + */ + suspend fun setToggleModeConfig(config: ToggleModeConfig) + + /** + * Observe toggle mode configuration changes + */ + fun observeToggleModeConfig(): Flow } diff --git a/app/src/main/java/com/supernova/networkswitch/domain/usecase/NetworkUseCases.kt b/app/src/main/java/com/supernova/networkswitch/domain/usecase/NetworkUseCases.kt index c843599..2b37404 100644 --- a/app/src/main/java/com/supernova/networkswitch/domain/usecase/NetworkUseCases.kt +++ b/app/src/main/java/com/supernova/networkswitch/domain/usecase/NetworkUseCases.kt @@ -2,13 +2,12 @@ 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.model.NetworkMode +import com.supernova.networkswitch.domain.model.ToggleModeConfig import com.supernova.networkswitch.domain.repository.NetworkControlRepository import com.supernova.networkswitch.domain.repository.PreferencesRepository import javax.inject.Inject -/** - * Use case for checking network control compatibility - */ class CheckCompatibilityUseCase @Inject constructor( private val networkControlRepository: NetworkControlRepository, private val preferencesRepository: PreferencesRepository @@ -19,18 +18,27 @@ class CheckCompatibilityUseCase @Inject constructor( } } -/** - * Use case for toggling network mode (4G/5G) - */ class ToggleNetworkModeUseCase @Inject constructor( - private val networkControlRepository: NetworkControlRepository + private val networkControlRepository: NetworkControlRepository, + private val preferencesRepository: PreferencesRepository ) { - suspend operator fun invoke(subId: Int): Result { + suspend operator fun invoke(subId: Int): Result { return try { - val currentState = networkControlRepository.getNetworkState(subId) - val newState = !currentState - networkControlRepository.setNetworkState(subId, newState) - .map { newState } + val toggleConfig = preferencesRepository.getToggleModeConfig() + + // Get the next mode to switch to (no current mode detection needed) + val targetMode = toggleConfig.getNextMode() + + // Set the network mode + val result = networkControlRepository.setNetworkMode(subId, targetMode) + + if (result.isSuccess) { + // Update the toggle state for next time + val newConfig = toggleConfig.toggle() + preferencesRepository.setToggleModeConfig(newConfig) + } + + result.map { targetMode } } catch (e: Exception) { Result.failure(e) } @@ -38,14 +46,14 @@ class ToggleNetworkModeUseCase @Inject constructor( } /** - * Use case for getting current network state + * Use case for getting current network mode */ -class GetNetworkStateUseCase @Inject constructor( +class GetCurrentNetworkModeUseCase @Inject constructor( private val networkControlRepository: NetworkControlRepository ) { - suspend operator fun invoke(subId: Int): Result { + suspend operator fun invoke(subId: Int): Result { return try { - Result.success(networkControlRepository.getNetworkState(subId)) + Result.success(networkControlRepository.getCurrentNetworkMode(subId)) } catch (e: Exception) { Result.failure(e) } @@ -64,12 +72,23 @@ class UpdateControlMethodUseCase @Inject constructor( } /** - * Use case for resetting network connections when switching methods + * Use case for getting toggle mode configuration */ -class ResetConnectionsUseCase @Inject constructor( - private val networkControlRepository: NetworkControlRepository +class GetToggleModeConfigUseCase @Inject constructor( + private val preferencesRepository: PreferencesRepository +) { + suspend operator fun invoke(): ToggleModeConfig { + return preferencesRepository.getToggleModeConfig() + } +} + +/** + * Use case for updating toggle mode configuration + */ +class UpdateToggleModeConfigUseCase @Inject constructor( + private val preferencesRepository: PreferencesRepository ) { - suspend operator fun invoke() { - networkControlRepository.resetConnections() + suspend operator fun invoke(config: ToggleModeConfig) { + preferencesRepository.setToggleModeConfig(config) } } diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/theme/Theme.kt b/app/src/main/java/com/supernova/networkswitch/presentation/theme/Theme.kt index 9e228a9..fe9f558 100644 --- a/app/src/main/java/com/supernova/networkswitch/presentation/theme/Theme.kt +++ b/app/src/main/java/com/supernova/networkswitch/presentation/theme/Theme.kt @@ -25,15 +25,6 @@ private val LightColorScheme = lightColorScheme( primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40 - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ ) @Composable diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/MainActivity.kt b/app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/MainActivity.kt index 9d35cce..1c08c2c 100644 --- a/app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/MainActivity.kt +++ b/app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/MainActivity.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Tune import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -38,6 +39,9 @@ class MainActivity : ComponentActivity() { viewModel = viewModel, onSettingsClick = { startActivity(Intent(this@MainActivity, SettingsActivity::class.java)) + }, + onNetworkModeConfigClick = { + startActivity(Intent(this@MainActivity, NetworkModeConfigActivity::class.java)) } ) } @@ -46,8 +50,6 @@ class MainActivity : ComponentActivity() { override fun onResume() { super.onResume() - // Refresh all data when app comes back from background - // This ensures UI reflects any changes made via Quick Settings tile or external changes viewModel.refreshAllData() } } @@ -56,7 +58,8 @@ class MainActivity : ComponentActivity() { @Composable private fun MainScreen( viewModel: MainViewModel, - onSettingsClick: () -> Unit + onSettingsClick: () -> Unit, + onNetworkModeConfigClick: () -> Unit ) { val compatibilityState = viewModel.compatibilityState @@ -65,6 +68,12 @@ private fun MainScreen( TopAppBar( title = { Text(stringResource(R.string.app_name)) }, actions = { + IconButton(onClick = onNetworkModeConfigClick) { + Icon( + imageVector = Icons.Default.Tune, + contentDescription = "Network Mode Configuration" + ) + } IconButton(onClick = onSettingsClick) { Icon( imageVector = Icons.Default.Settings, @@ -93,7 +102,8 @@ private fun MainScreen( // Network Toggle Card (show if compatible) if (compatibilityState is CompatibilityState.Compatible) { NetworkToggleCard( - networkState = viewModel.networkState, + currentMode = viewModel.currentNetworkMode, + toggleButtonText = viewModel.getToggleButtonText(), isLoading = viewModel.isLoading, onToggleClick = { viewModel.toggleNetworkMode() } ) diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/NetworkModeConfigActivity.kt b/app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/NetworkModeConfigActivity.kt new file mode 100644 index 0000000..859b90e --- /dev/null +++ b/app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/NetworkModeConfigActivity.kt @@ -0,0 +1,219 @@ +package com.supernova.networkswitch.presentation.ui.activity + +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.supernova.networkswitch.R +import com.supernova.networkswitch.domain.model.NetworkMode +import com.supernova.networkswitch.presentation.theme.NetworkSwitchTheme +import com.supernova.networkswitch.presentation.viewmodel.NetworkModeConfigViewModel +import com.supernova.networkswitch.presentation.ui.composable.NetworkModeSelector +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class NetworkModeConfigActivity : ComponentActivity() { + + private val viewModel: NetworkModeConfigViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + NetworkSwitchTheme { + NetworkModeConfigScreen( + viewModel = viewModel, + onBackClick = { finish() } + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun NetworkModeConfigScreen( + viewModel: NetworkModeConfigViewModel, + onBackClick: () -> Unit +) { + val context = LocalContext.current + val currentConfig by viewModel.currentConfig.collectAsState() + val isLoading by viewModel.isLoading.collectAsState() + val configSaved by viewModel.configSaved.collectAsState() + + // Show toast when configuration is saved + LaunchedEffect(configSaved) { + if (configSaved) { + Toast.makeText(context, context.getString(R.string.configuration_saved), Toast.LENGTH_SHORT).show() + viewModel.resetSavedState() + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.network_mode_config)) }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Description Card + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = stringResource(R.string.network_mode_config), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.network_mode_config_desc), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + // Mode A Selection + NetworkModeSelector( + label = stringResource(R.string.mode_a_label), + selectedMode = currentConfig.modeA, + onModeSelected = { mode -> + viewModel.updateModeA(mode) + } + ) + + // Mode B Selection + NetworkModeSelector( + label = stringResource(R.string.mode_b_label), + selectedMode = currentConfig.modeB, + onModeSelected = { mode -> + viewModel.updateModeB(mode) + } + ) + + // Current Configuration Preview + Card( + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = "Configuration Preview", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Toggle will switch between:", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "• ${currentConfig.modeA.displayName}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary + ) + + Text( + text = "• ${currentConfig.modeB.displayName}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary + ) + } + } + + // Save Button + Button( + onClick = { viewModel.saveConfiguration() }, + enabled = !isLoading && currentConfig.modeA != currentConfig.modeB, + modifier = Modifier.fillMaxWidth() + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp + ) + Spacer(modifier = Modifier.width(8.dp)) + } + Text(stringResource(R.string.save_configuration)) + } + + // Warning if both modes are the same + if (currentConfig.modeA == currentConfig.modeB) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "⚠️", + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Mode A and Mode B must be different", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onErrorContainer + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/SettingsActivity.kt b/app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/SettingsActivity.kt index 2211722..f7d08a7 100644 --- a/app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/SettingsActivity.kt +++ b/app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/SettingsActivity.kt @@ -12,7 +12,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Refresh @@ -63,7 +63,7 @@ private fun SettingsScreen( navigationIcon = { IconButton(onClick = onBackClick) { Icon( - imageVector = Icons.Default.ArrowBack, + imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back" ) } diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/ui/components/SharedComponents.kt b/app/src/main/java/com/supernova/networkswitch/presentation/ui/components/SharedComponents.kt index 7c0b5b1..d1c1462 100644 --- a/app/src/main/java/com/supernova/networkswitch/presentation/ui/components/SharedComponents.kt +++ b/app/src/main/java/com/supernova/networkswitch/presentation/ui/components/SharedComponents.kt @@ -1,95 +1,13 @@ package com.supernova.networkswitch.presentation.ui.components import androidx.compose.foundation.layout.* -import androidx.compose.foundation.selection.selectable -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Error import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.supernova.networkswitch.domain.model.CompatibilityState -import com.supernova.networkswitch.domain.model.ControlMethod -/** - * Reusable UI components to reduce code duplication - */ - -@Composable -fun StatusIndicator( - state: CompatibilityState, - modifier: Modifier = Modifier -) { - when (state) { - is CompatibilityState.Pending -> CircularProgressIndicator( - modifier = modifier.size(20.dp), - strokeWidth = 2.dp - ) - is CompatibilityState.Compatible -> Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = "Compatible", - tint = MaterialTheme.colorScheme.primary, - modifier = modifier.size(20.dp) - ) - is CompatibilityState.PermissionDenied -> Icon( - imageVector = Icons.Default.Error, - contentDescription = "Permission denied", - tint = MaterialTheme.colorScheme.error, - modifier = modifier.size(20.dp) - ) - is CompatibilityState.Incompatible -> Icon( - imageVector = Icons.Default.Error, - contentDescription = "Not available", - tint = MaterialTheme.colorScheme.error, - modifier = modifier.size(20.dp) - ) - } -} - -@Composable -fun MethodSelectionRow( - method: ControlMethod, - title: String, - description: String, - selected: Boolean, - compatibilityState: CompatibilityState, - onMethodSelected: (ControlMethod) -> Unit, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier - .fillMaxWidth() - .selectable( - selected = selected, - onClick = { onMethodSelected(method) } - ) - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selected, - onClick = { onMethodSelected(method) } - ) - Spacer(modifier = Modifier.width(8.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium - ) - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - StatusIndicator(state = compatibilityState) - } -} @Composable fun CardSection( @@ -120,74 +38,3 @@ fun CardSection( } } } - -@Composable -fun StatusText( - compatibilityState: CompatibilityState, - currentMethod: ControlMethod? = null -) { - when (compatibilityState) { - is CompatibilityState.Pending -> { - Text( - text = "Checking compatibility...", - style = MaterialTheme.typography.bodyMedium - ) - } - is CompatibilityState.Compatible -> { - Text( - text = "✓ Compatible", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary - ) - } - is CompatibilityState.PermissionDenied -> { - val methodName = when (compatibilityState.method) { - ControlMethod.ROOT -> "Root" - ControlMethod.SHIZUKU -> "Shizuku" - } - Text( - text = "✗ $methodName Permission Denied", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.error - ) - Text( - text = when (compatibilityState.method) { - ControlMethod.ROOT -> "Grant root access or try Shizuku method" - ControlMethod.SHIZUKU -> "Grant Shizuku permission or try Root method" - }, - style = MaterialTheme.typography.bodySmall, - textAlign = TextAlign.Center - ) - } - is CompatibilityState.Incompatible -> { - Text( - text = "✗ Not Compatible", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.error - ) - } - } -} - -@Composable -fun NetworkModeToggleButton( - networkState: Boolean, - isLoading: Boolean, - onToggleClick: () -> Unit, - modifier: Modifier = Modifier -) { - Button( - onClick = onToggleClick, - enabled = !isLoading, - modifier = modifier.fillMaxWidth() - ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp - ) - Spacer(modifier = Modifier.width(8.dp)) - } - Text(text = if (networkState) "Switch to 4G" else "Switch to 5G") - } -} diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/NetworkModeSelector.kt b/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/NetworkModeSelector.kt new file mode 100644 index 0000000..b9c525f --- /dev/null +++ b/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/NetworkModeSelector.kt @@ -0,0 +1,79 @@ +package com.supernova.networkswitch.presentation.ui.composable + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.supernova.networkswitch.domain.model.NetworkMode + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NetworkModeSelector( + label: String, + selectedMode: NetworkMode, + onModeSelected: (NetworkMode) -> Unit, + modifier: Modifier = Modifier, + availableModes: List = NetworkMode.values().toList() +) { + var expanded by remember { mutableStateOf(false) } + + Card(modifier = modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded } + ) { + OutlinedTextField( + value = selectedMode.displayName, + onValueChange = { }, + readOnly = true, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) + }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + modifier = Modifier + .fillMaxWidth() + .menuAnchor() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + availableModes.forEach { mode -> + DropdownMenuItem( + text = { + Column { + Text( + text = mode.displayName, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + } + }, + onClick = { + onModeSelected(mode) + expanded = false + } + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/NetworkSwitchComponents.kt b/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/NetworkSwitchComponents.kt index c459fb8..36c23b3 100644 --- a/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/NetworkSwitchComponents.kt +++ b/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/NetworkSwitchComponents.kt @@ -14,8 +14,10 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.supernova.networkswitch.domain.model.CompatibilityState import com.supernova.networkswitch.domain.model.ControlMethod +import com.supernova.networkswitch.domain.model.NetworkMode import com.supernova.networkswitch.presentation.ui.components.CardSection -import com.supernova.networkswitch.presentation.ui.components.NetworkModeToggleButton + +private fun ControlMethod.displayName() = if (this == ControlMethod.SHIZUKU) "Shizuku" else "Root" @Composable fun CompatibilityCard( @@ -57,7 +59,7 @@ fun CompatibilityCard( textAlign = TextAlign.Center ) Text( - text = "Using ${if (currentControlMethod == ControlMethod.SHIZUKU) "Shizuku" else "Root"} method", + text = "Using ${currentControlMethod.displayName()} method", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.primary, @@ -74,7 +76,7 @@ fun CompatibilityCard( ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = "${if (compatibilityState.method == ControlMethod.ROOT) "Root" else "Shizuku"} Access Denied", + text = "${compatibilityState.method.displayName()} Access Denied", style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center ) @@ -120,7 +122,8 @@ fun CompatibilityCard( @Composable fun NetworkToggleCard( - networkState: Boolean, + currentMode: NetworkMode?, + toggleButtonText: String, isLoading: Boolean, onToggleClick: () -> Unit, modifier: Modifier = Modifier @@ -132,18 +135,18 @@ fun NetworkToggleCard( Spacer(modifier = Modifier.height(16.dp)) Text( - text = if (networkState) "5G Mode Active" else "4G Mode Active", + text = if (currentMode != null) "Current: ${currentMode.displayName}" else "Network mode unavailable", style = MaterialTheme.typography.titleMedium, - color = if (networkState) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary + color = if (currentMode != null) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = if (networkState) { - "Switch to pure 4G mode (LTE only) for better battery life" + text = if (currentMode != null) { + "Tap to switch to the configured alternate network mode" } else { - "Switch to pure 5G mode (NR only) for maximum data speeds" + "Unable to detect current network mode" }, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, @@ -152,11 +155,20 @@ fun NetworkToggleCard( Spacer(modifier = Modifier.height(16.dp)) - NetworkModeToggleButton( - networkState = networkState, - isLoading = isLoading, - onToggleClick = onToggleClick - ) + Button( + onClick = onToggleClick, + enabled = !isLoading && currentMode != null, + modifier = Modifier.fillMaxWidth() + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp + ) + Spacer(modifier = Modifier.width(8.dp)) + } + Text(text = toggleButtonText) + } } } @@ -182,7 +194,7 @@ fun QuickSettingsHintCard(modifier: Modifier = Modifier) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "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.", + text = "Add the \"Network Switch Toggle\" tile to your Quick Settings for instant network switching. Pull down your notification panel, tap the pencil icon, and add the tile.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/viewmodel/MainViewModel.kt b/app/src/main/java/com/supernova/networkswitch/presentation/viewmodel/MainViewModel.kt index aab9cab..f59b820 100644 --- a/app/src/main/java/com/supernova/networkswitch/presentation/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/supernova/networkswitch/presentation/viewmodel/MainViewModel.kt @@ -8,25 +8,26 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.getValue import com.supernova.networkswitch.domain.model.CompatibilityState import com.supernova.networkswitch.domain.model.ControlMethod +import com.supernova.networkswitch.domain.model.NetworkMode +import com.supernova.networkswitch.domain.model.ToggleModeConfig import com.supernova.networkswitch.domain.usecase.CheckCompatibilityUseCase -import com.supernova.networkswitch.domain.usecase.GetNetworkStateUseCase +import com.supernova.networkswitch.domain.usecase.GetCurrentNetworkModeUseCase import com.supernova.networkswitch.domain.usecase.ToggleNetworkModeUseCase import com.supernova.networkswitch.domain.usecase.UpdateControlMethodUseCase +import com.supernova.networkswitch.domain.usecase.GetToggleModeConfigUseCase import com.supernova.networkswitch.domain.repository.PreferencesRepository import kotlinx.coroutines.launch import kotlinx.coroutines.flow.collectLatest import javax.inject.Inject import dagger.hilt.android.lifecycle.HiltViewModel -/** - * MainViewModel with clean architecture using domain models and use cases - */ @HiltViewModel class MainViewModel @Inject constructor( private val checkCompatibilityUseCase: CheckCompatibilityUseCase, - private val getNetworkStateUseCase: GetNetworkStateUseCase, + private val getCurrentNetworkModeUseCase: GetCurrentNetworkModeUseCase, private val toggleNetworkModeUseCase: ToggleNetworkModeUseCase, private val updateControlMethodUseCase: UpdateControlMethodUseCase, + private val getToggleModeConfigUseCase: GetToggleModeConfigUseCase, private val preferencesRepository: PreferencesRepository ) : ViewModel() { @@ -34,8 +35,12 @@ class MainViewModel @Inject constructor( var selectedMethod by mutableStateOf(ControlMethod.SHIZUKU) private set - // Network state - current 5G enabled status - var networkState by mutableStateOf(false) + // Current network mode + var currentNetworkMode by mutableStateOf(null) + private set + + // Toggle mode configuration + var toggleModeConfig by mutableStateOf(ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY)) private set var isLoading by mutableStateOf(false) @@ -47,6 +52,7 @@ class MainViewModel @Inject constructor( init { observeControlMethodPreference() + loadToggleModeConfig() checkCompatibility() refreshNetworkState() } @@ -60,10 +66,20 @@ class MainViewModel @Inject constructor( if (selectedMethod != preferredMethod) { selectedMethod = preferredMethod checkCompatibility() + loadToggleModeConfig() refreshNetworkState() } } } + + // Also observe toggle mode config changes + viewModelScope.launch { + preferencesRepository.observeToggleModeConfig().collectLatest { newConfig -> + toggleModeConfig = newConfig + // Force UI update when config changes + refreshNetworkState() + } + } } /** @@ -101,7 +117,21 @@ class MainViewModel @Inject constructor( } /** - * Toggle network mode using domain use case + * Load toggle mode configuration + */ + private fun loadToggleModeConfig() { + viewModelScope.launch { + try { + toggleModeConfig = getToggleModeConfigUseCase() + } catch (e: Exception) { + // Use default configuration if loading fails + toggleModeConfig = ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY) + } + } + } + + /** + * Toggle network mode using configurable modes */ fun toggleNetworkMode() { if (isLoading) return @@ -111,8 +141,8 @@ class MainViewModel @Inject constructor( val subId = SubscriptionManager.getDefaultDataSubscriptionId() toggleNetworkModeUseCase(subId) - .onSuccess { newState -> - networkState = newState + .onSuccess { newMode -> + currentNetworkMode = newMode } .onFailure { // On failure, refresh state to get current status @@ -123,6 +153,14 @@ class MainViewModel @Inject constructor( } } + /** + * Get display text for current network mode and next toggle mode + */ + fun getToggleButtonText(): String { + val nextMode = toggleModeConfig.getNextMode() + return "Switch to ${nextMode.displayName}" + } + /** * Refresh current network state */ @@ -130,12 +168,12 @@ class MainViewModel @Inject constructor( viewModelScope.launch { val subId = SubscriptionManager.getDefaultDataSubscriptionId() - getNetworkStateUseCase(subId) - .onSuccess { state -> - networkState = state + getCurrentNetworkModeUseCase(subId) + .onSuccess { mode -> + currentNetworkMode = mode } .onFailure { - networkState = false + currentNetworkMode = null } } } diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/viewmodel/NetworkModeConfigViewModel.kt b/app/src/main/java/com/supernova/networkswitch/presentation/viewmodel/NetworkModeConfigViewModel.kt new file mode 100644 index 0000000..b27cfcb --- /dev/null +++ b/app/src/main/java/com/supernova/networkswitch/presentation/viewmodel/NetworkModeConfigViewModel.kt @@ -0,0 +1,81 @@ +package com.supernova.networkswitch.presentation.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.supernova.networkswitch.domain.model.NetworkMode +import com.supernova.networkswitch.domain.model.ToggleModeConfig +import com.supernova.networkswitch.domain.usecase.GetToggleModeConfigUseCase +import com.supernova.networkswitch.domain.usecase.UpdateToggleModeConfigUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * ViewModel for network mode configuration screen + */ +@HiltViewModel +class NetworkModeConfigViewModel @Inject constructor( + private val getToggleModeConfigUseCase: GetToggleModeConfigUseCase, + private val updateToggleModeConfigUseCase: UpdateToggleModeConfigUseCase +) : ViewModel() { + + private val _currentConfig = MutableStateFlow( + ToggleModeConfig(NetworkMode.LTE_ONLY, NetworkMode.NR_ONLY) + ) + val currentConfig: StateFlow = _currentConfig.asStateFlow() + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + + private val _configSaved = MutableStateFlow(false) + val configSaved: StateFlow = _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" }