From 72d53df48462ef91cec4251827cb7a98b0a71e9e Mon Sep 17 00:00:00 2001 From: s4tyendra Date: Thu, 2 Oct 2025 15:34:47 +0530 Subject: [PATCH 1/5] feat: Upgrade to Material 3 Expressive This commit upgrades the project to use the new Material 3 Expressive library (`1.5.0-alpha04`). Key changes include: * Updating the Material 3 dependency to `material3Expressive`. * Replacing `MaterialTheme` with `MaterialExpressiveTheme` and enabling `MotionScheme.expressive()`. * Enabling edge-to-edge display in `MainActivity`. * Replacing `@ExperimentalMaterial3Api` with `@ExperimentalMaterial3ExpressiveApi`. * Enabling the predictive back gesture in `AndroidManifest.xml`. * Removing manual status bar color handling, now managed by the theme. --- app/src/main/AndroidManifest.xml | 1 + .../networkswitch/presentation/theme/Theme.kt | 22 +++++--------- .../presentation/ui/activity/MainActivity.kt | 5 ++-- .../ui/activity/NetworkModeConfigActivity.kt | 2 +- .../ui/activity/SettingsActivity.kt | 2 +- .../ui/composable/NetworkModeSelector.kt | 2 +- .../ui/composable/NetworkSwitchComponents.kt | 30 ++++++++++--------- gradle/libs.versions.toml | 3 +- 8 files changed, 32 insertions(+), 35 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3552c09..e818558 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.NetworkSwitch" + android:enableOnBackInvokedCallback="true" tools:targetApi="31"> DarkColorScheme else -> LightColorScheme } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme - } - } - - MaterialTheme( + MaterialExpressiveTheme( colorScheme = colorScheme, typography = Typography, + motionScheme = MotionScheme.expressive(), content = content ) } 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 1c08c2c..07ac4cb 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 @@ -4,6 +4,7 @@ import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -32,7 +33,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + enableEdgeToEdge() setContent { NetworkSwitchTheme { MainScreen( @@ -54,7 +55,7 @@ class MainActivity : ComponentActivity() { } } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun MainScreen( viewModel: MainViewModel, 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 index 859b90e..71952e0 100644 --- 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 @@ -44,7 +44,7 @@ class NetworkModeConfigActivity : ComponentActivity() { } } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun NetworkModeConfigScreen( viewModel: NetworkModeConfigViewModel, 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 f7d08a7..0c8f40b 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 @@ -48,7 +48,7 @@ class SettingsActivity : ComponentActivity() { } } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun SettingsScreen( viewModel: SettingsViewModel, 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 index b9c525f..4df92a6 100644 --- 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 @@ -9,7 +9,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.supernova.networkswitch.domain.model.NetworkMode -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun NetworkModeSelector( label: String, 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 36c23b3..e2909f1 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 @@ -19,6 +19,7 @@ import com.supernova.networkswitch.presentation.ui.components.CardSection private fun ControlMethod.displayName() = if (this == ControlMethod.SHIZUKU) "Shizuku" else "Root" +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun CompatibilityCard( compatibilityState: CompatibilityState, @@ -43,7 +44,7 @@ fun CompatibilityCard( textAlign = TextAlign.Center ) } - + is CompatibilityState.Compatible -> { Icon( imageVector = Icons.Default.CheckCircle, @@ -66,7 +67,7 @@ fun CompatibilityCard( fontWeight = FontWeight.Medium ) } - + is CompatibilityState.PermissionDenied -> { Icon( imageVector = Icons.Default.Warning, @@ -81,9 +82,9 @@ fun CompatibilityCard( textAlign = TextAlign.Center ) Text( - text = if (compatibilityState.method == ControlMethod.ROOT) - "Please grant root access to use this app" - else + text = if (compatibilityState.method == ControlMethod.ROOT) + "Please grant root access to use this app" + else "Please grant Shizuku permission or install Shizuku", style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, @@ -92,7 +93,7 @@ fun CompatibilityCard( Spacer(modifier = Modifier.height(16.dp)) Button(onClick = onRetryClick) { Text("Retry") } } - + is CompatibilityState.Incompatible -> { Icon( imageVector = Icons.Default.Error, @@ -113,7 +114,8 @@ fun CompatibilityCard( color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = onRetryClick) { Text("Retry") } + Button(onClick = onRetryClick, shapes = ButtonDefaults.shapes()) { Text("Retry") } + } } } @@ -133,15 +135,15 @@ fun NetworkToggleCard( modifier = modifier ) { Spacer(modifier = Modifier.height(16.dp)) - + Text( text = if (currentMode != null) "Current: ${currentMode.displayName}" else "Network mode unavailable", style = MaterialTheme.typography.titleMedium, color = if (currentMode != null) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) - + Spacer(modifier = Modifier.height(8.dp)) - + Text( text = if (currentMode != null) { "Tap to switch to the configured alternate network mode" @@ -152,9 +154,9 @@ fun NetworkToggleCard( textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant ) - + Spacer(modifier = Modifier.height(16.dp)) - + Button( onClick = onToggleClick, enabled = !isLoading && currentMode != null, @@ -190,9 +192,9 @@ fun QuickSettingsHintCard(modifier: Modifier = Modifier) { style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) - + Spacer(modifier = Modifier.height(8.dp)) - + Text( 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, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a1b1ffd..fe132bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ hilt = "2.48" datastore = "1.1.1" mockk = "1.13.10" coroutinesTest = "1.8.0" +material3Expressive = "1.5.0-alpha04" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -31,7 +32,7 @@ androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 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-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3Expressive" } 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" } From 0c837e5d2b70e05624befd5bfca81db0b06374b7 Mon Sep 17 00:00:00 2001 From: s4tyendra Date: Thu, 2 Oct 2025 15:52:03 +0530 Subject: [PATCH 2/5] feat: Add permission request flow for control methods This commit introduces a mechanism to explicitly request permissions for the selected control method (Root or Shizuku) directly within the app. If the initial compatibility check results in a `PermissionDenied` state, the user can now tap a "retry" action, which will trigger the appropriate permission request dialog (e.g., for root access or Shizuku). Key changes include: * Adding a `requestPermission` function to the `NetworkControlRepository` and its underlying data sources. * Implementing the permission request logic for both `RootNetworkControlDataSource` (using `Shell.getShell()`) and `ShizukuNetworkControlDataSource`. * Creating a `RequestPermissionUseCase` to abstract the permission request flow. * Updating `MainViewModel` to use this new use case, allowing `retryCompatibilityCheck` to request permissions before re-checking. --- .../NetworkControlRepositoryImpl.kt | 5 +++ .../data/source/NetworkControlDataSource.kt | 1 + .../source/RootNetworkControlDataSource.kt | 12 ++++++ .../source/ShizukuNetworkControlDataSource.kt | 40 +++++++++++++++++++ .../domain/repository/Repositories.kt | 5 +++ .../domain/usecase/NetworkUseCases.kt | 13 ++++++ .../presentation/viewmodel/MainViewModel.kt | 22 ++++++++-- 7 files changed, 95 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/supernova/networkswitch/data/repository/NetworkControlRepositoryImpl.kt b/app/src/main/java/com/supernova/networkswitch/data/repository/NetworkControlRepositoryImpl.kt index 3881e56..6ea5f92 100644 --- a/app/src/main/java/com/supernova/networkswitch/data/repository/NetworkControlRepositoryImpl.kt +++ b/app/src/main/java/com/supernova/networkswitch/data/repository/NetworkControlRepositoryImpl.kt @@ -56,6 +56,11 @@ class NetworkControlRepositoryImpl @Inject constructor( shizukuDataSource.resetConnection() } + override suspend fun requestPermission(method: ControlMethod): Boolean { + val dataSource = getDataSource(method) + return dataSource.requestPermission() + } + private fun getDataSource(method: ControlMethod): NetworkControlDataSource { return when (method) { ControlMethod.ROOT -> rootDataSource 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 d6fa53c..7b8ba55 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 @@ -9,4 +9,5 @@ interface NetworkControlDataSource { suspend fun setNetworkMode(subId: Int, mode: NetworkMode) fun isConnected(): Boolean fun resetConnection() + suspend fun requestPermission(): Boolean } 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 26b1748..8aff649 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 @@ -10,6 +10,7 @@ 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.Shell import com.topjohnwu.superuser.ipc.RootService import kotlinx.coroutines.suspendCancellableCoroutine import javax.inject.Inject @@ -34,6 +35,17 @@ class RootNetworkControlDataSource @Inject constructor( } } + override suspend fun requestPermission(): Boolean { + return try { + // Request root access by attempting to get a shell + // This will trigger the root permission dialog if not already granted + val shell = Shell.getShell() + Shell.isAppGrantedRoot() == true + } catch (e: Exception) { + false + } + } + override suspend fun getCurrentNetworkMode(subId: Int): NetworkMode? { return try { val controller = getNetworkController() 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 90e7e8e..07e2406 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 @@ -157,4 +157,44 @@ class ShizukuNetworkControlDataSource @Inject constructor( false } } + + override suspend fun requestPermission(): Boolean { + return try { + if (!Shizuku.pingBinder()) { + return false + } + if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { + return true + } + suspendCancellableCoroutine { continuation -> + val permissionListener = object : Shizuku.OnRequestPermissionResultListener { + override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { + if (requestCode == SHIZUKU_PERMISSION_REQUEST_ID) { + Shizuku.removeRequestPermissionResultListener(this) + val granted = grantResult == PackageManager.PERMISSION_GRANTED + if (continuation.isActive) { + continuation.resume(granted) + } + } + } + } + + continuation.invokeOnCancellation { + Shizuku.removeRequestPermissionResultListener(permissionListener) + } + + try { + Shizuku.addRequestPermissionResultListener(permissionListener) + Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_ID) + } catch (e: Exception) { + Shizuku.removeRequestPermissionResultListener(permissionListener) + if (continuation.isActive) { + continuation.resume(false) + } + } + } + } catch (e: Exception) { + false + } + } } 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 047b601..3d36256 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 @@ -29,6 +29,11 @@ interface NetworkControlRepository { * Reset connections - useful when switching control methods */ suspend fun resetConnections() + + /** + * Request permission for the specified control method + */ + suspend fun requestPermission(method: ControlMethod): Boolean } /** 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 2b37404..eae5c0c 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 @@ -92,3 +92,16 @@ class UpdateToggleModeConfigUseCase @Inject constructor( preferencesRepository.setToggleModeConfig(config) } } + +/** + * Use case for requesting permission for a specific control method + */ +class RequestPermissionUseCase @Inject constructor( + private val networkControlRepository: NetworkControlRepository, + private val preferencesRepository: PreferencesRepository +) { + suspend operator fun invoke(method: ControlMethod? = null): Boolean { + val controlMethod = method ?: preferencesRepository.getControlMethod() + return networkControlRepository.requestPermission(controlMethod) + } +} 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 f59b820..9e56c19 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 @@ -15,6 +15,7 @@ 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.usecase.RequestPermissionUseCase import com.supernova.networkswitch.domain.repository.PreferencesRepository import kotlinx.coroutines.launch import kotlinx.coroutines.flow.collectLatest @@ -28,6 +29,7 @@ class MainViewModel @Inject constructor( private val toggleNetworkModeUseCase: ToggleNetworkModeUseCase, private val updateControlMethodUseCase: UpdateControlMethodUseCase, private val getToggleModeConfigUseCase: GetToggleModeConfigUseCase, + private val requestPermissionUseCase: RequestPermissionUseCase, private val preferencesRepository: PreferencesRepository ) : ViewModel() { @@ -102,12 +104,26 @@ class MainViewModel @Inject constructor( } /** - * Retry compatibility check + * Retry compatibility check with request permissions if denied */ fun retryCompatibilityCheck() { - checkCompatibility() + viewModelScope.launch { + val currentState = compatibilityState + if (currentState is CompatibilityState.PermissionDenied) { + compatibilityState = CompatibilityState.Pending + val permissionGranted = requestPermissionUseCase(currentState.method) + compatibilityState = if (permissionGranted) { + checkCompatibilityUseCase() + } else { + // denied :( + currentState + } + } else { + checkCompatibility() + } + } } - + /** * Refresh all data when app resumes */ From 09e36c4b04eacd3b9a1a16ab7ebd581ea5e07b2b Mon Sep 17 00:00:00 2001 From: s4tyendra Date: Thu, 2 Oct 2025 15:55:03 +0530 Subject: [PATCH 3/5] Refactor: Use default button shapes for Material 3 Expressive This commit updates several `Button` components to explicitly use the default shapes provided by `ButtonDefaults.shapes()`. This change ensures the buttons correctly adopt the new squircle shape introduced in the Material 3 Expressive library, rather than retaining the previous pill shape. An `@OptIn` for `ExperimentalMaterial3ExpressiveApi` was also added where necessary. --- .../presentation/ui/activity/NetworkModeConfigActivity.kt | 3 ++- .../presentation/ui/activity/SettingsActivity.kt | 2 +- .../presentation/ui/composable/NetworkSwitchComponents.kt | 6 ++++-- 3 files changed, 7 insertions(+), 4 deletions(-) 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 index 71952e0..77afa7f 100644 --- 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 @@ -175,7 +175,8 @@ private fun NetworkModeConfigScreen( Button( onClick = { viewModel.saveConfiguration() }, enabled = !isLoading && currentConfig.modeA != currentConfig.modeB, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + shapes = ButtonDefaults.shapes() ) { if (isLoading) { CircularProgressIndicator( 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 0c8f40b..91c3162 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 @@ -61,7 +61,7 @@ private fun SettingsScreen( TopAppBar( title = { Text("Settings") }, navigationIcon = { - IconButton(onClick = onBackClick) { + IconButton(onClick = onBackClick,) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back" 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 e2909f1..00c907c 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 @@ -91,7 +91,7 @@ fun CompatibilityCard( color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = onRetryClick) { Text("Retry") } + Button(onClick = onRetryClick,shapes = ButtonDefaults.shapes()) { Text("Retry") } } is CompatibilityState.Incompatible -> { @@ -122,6 +122,7 @@ fun CompatibilityCard( } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun NetworkToggleCard( currentMode: NetworkMode?, @@ -160,7 +161,8 @@ fun NetworkToggleCard( Button( onClick = onToggleClick, enabled = !isLoading && currentMode != null, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + shapes = ButtonDefaults.shapes() ) { if (isLoading) { CircularProgressIndicator( From 5e1a91d4b5ad6ae2b58b16f478bcd07d713c40c9 Mon Sep 17 00:00:00 2001 From: s4tyendra Date: Thu, 2 Oct 2025 18:28:53 +0530 Subject: [PATCH 4/5] refactor: Consolidate settings and config into bottom sheets This commit refactors the user interface by removing the separate `SettingsActivity` and `NetworkModeConfigActivity` and consolidating their functionality into modal bottom sheets accessible directly from the `MainActivity`. This change simplifies the app's navigation and provides a more integrated and fluid user experience. Key changes include: * **UI Consolidation**: Deleted `NetworkModeConfigActivity.kt` and renamed `SettingsActivity.kt` to `SettingsBottomSheet.kt`. * **New Bottom Sheets**: * Created `SettingsBottomSheet` which now combines the Control Method selection and Network Mode configuration. * Introduced an "About" bottom sheet, accessible from the main screen. * **Main Screen Update**: * Replaced the navigation to separate activities with actions to show the new `SettingsBottomSheet` and `AboutBottomSheet`. * Added an "Info" icon to the top app bar for the About sheet. * **Enhanced Quick Settings Card**: * The card now checks if the Quick Settings tile has been added by the user. * On Android 13+ (Tiramisu), it automatically prompts the user to add the tile. * The card's content dynamically updates to reflect whether the tile is already added or to provide instructions. * **Component Refactoring**: * The "About" section was extracted into a dedicated `AboutCard.kt` composable with an improved layout. * `NetworkModeSelector` was updated to handle a larger number of options with a scrollable dropdown. --- .../presentation/ui/activity/MainActivity.kt | 93 +++- .../ui/activity/NetworkModeConfigActivity.kt | 220 --------- .../ui/activity/SettingsActivity.kt | 369 --------------- .../presentation/ui/composable/AboutCard.kt | 119 +++++ .../ui/composable/NetworkModeSelector.kt | 93 ++-- .../ui/composable/NetworkSwitchComponents.kt | 141 +++++- .../ui/composable/SettingsBottomSheet.kt | 424 ++++++++++++++++++ .../service/NetworkTileService.kt | 14 +- 8 files changed, 788 insertions(+), 685 deletions(-) delete mode 100644 app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/NetworkModeConfigActivity.kt delete mode 100644 app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/SettingsActivity.kt create mode 100644 app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/AboutCard.kt create mode 100644 app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/SettingsBottomSheet.kt 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 07ac4cb..b65a690 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 @@ -1,6 +1,5 @@ package com.supernova.networkswitch.presentation.ui.activity -import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -10,17 +9,23 @@ 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.filled.Info 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.supernova.networkswitch.BuildConfig import com.supernova.networkswitch.R import com.supernova.networkswitch.domain.model.CompatibilityState import com.supernova.networkswitch.presentation.theme.NetworkSwitchTheme import com.supernova.networkswitch.presentation.viewmodel.MainViewModel +import com.supernova.networkswitch.presentation.viewmodel.NetworkModeConfigViewModel +import com.supernova.networkswitch.presentation.viewmodel.SettingsViewModel +import com.supernova.networkswitch.presentation.ui.composable.AboutCard +import com.supernova.networkswitch.presentation.ui.composable.SettingsBottomSheet import com.supernova.networkswitch.presentation.ui.composable.CompatibilityCard import com.supernova.networkswitch.presentation.ui.composable.NetworkToggleCard import com.supernova.networkswitch.presentation.ui.composable.QuickSettingsHintCard @@ -30,6 +35,8 @@ import dagger.hilt.android.AndroidEntryPoint class MainActivity : ComponentActivity() { private val viewModel: MainViewModel by viewModels() + private val settingsViewModel: SettingsViewModel by viewModels() + private val networkModeConfigViewModel: NetworkModeConfigViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -38,12 +45,8 @@ class MainActivity : ComponentActivity() { NetworkSwitchTheme { MainScreen( viewModel = viewModel, - onSettingsClick = { - startActivity(Intent(this@MainActivity, SettingsActivity::class.java)) - }, - onNetworkModeConfigClick = { - startActivity(Intent(this@MainActivity, NetworkModeConfigActivity::class.java)) - } + settingsViewModel = settingsViewModel, + networkModeConfigViewModel = networkModeConfigViewModel ) } } @@ -59,26 +62,80 @@ class MainActivity : ComponentActivity() { @Composable private fun MainScreen( viewModel: MainViewModel, - onSettingsClick: () -> Unit, - onNetworkModeConfigClick: () -> Unit + settingsViewModel: SettingsViewModel, + networkModeConfigViewModel: NetworkModeConfigViewModel ) { val compatibilityState = viewModel.compatibilityState - + val aboutBottomSheetState = rememberModalBottomSheetState() + val settingsBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var showAboutBottomSheet by remember { mutableStateOf(false) } + var showSettingsBottomSheet by remember { mutableStateOf(false) } + + // Settings state + val controlMethod by settingsViewModel.controlMethod.collectAsState() + val currentConfig by networkModeConfigViewModel.currentConfig.collectAsState() + + if (showAboutBottomSheet) { + ModalBottomSheet( + onDismissRequest = { showAboutBottomSheet = false }, + sheetState = aboutBottomSheetState + ) { + AboutCard() + Spacer(modifier = Modifier.height(32.dp)) + } + } + + if (showSettingsBottomSheet) { + ModalBottomSheet( + onDismissRequest = { showSettingsBottomSheet = false }, + sheetState = settingsBottomSheetState + ) { + SettingsBottomSheet( + selectedControlMethod = controlMethod, + onControlMethodSelected = { settingsViewModel.updateControlMethod(it) }, + rootCompatibility = settingsViewModel.rootCompatibility, + shizukuCompatibility = settingsViewModel.shizukuCompatibility, + onRetryCompatibilityClick = { settingsViewModel.retryCompatibilityCheck() }, + currentConfig = currentConfig, + onModeASelected = { mode -> + networkModeConfigViewModel.updateModeA(mode) + networkModeConfigViewModel.saveConfiguration() + }, + onModeBSelected = { mode -> + networkModeConfigViewModel.updateModeB(mode) + networkModeConfigViewModel.saveConfiguration() + } + ) + } + } + Scaffold( topBar = { TopAppBar( - title = { Text(stringResource(R.string.app_name)) }, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(stringResource(R.string.app_name)) + if (BuildConfig.DEBUG) { + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "DEBUG", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error + ) + } + } + }, actions = { - IconButton(onClick = onNetworkModeConfigClick) { + IconButton(onClick = { showSettingsBottomSheet = true }) { Icon( - imageVector = Icons.Default.Tune, - contentDescription = "Network Mode Configuration" + imageVector = Icons.Default.Settings, + contentDescription = "Settings" ) } - IconButton(onClick = onSettingsClick) { + IconButton(onClick = { showAboutBottomSheet = true }) { Icon( - imageVector = Icons.Default.Settings, - contentDescription = "Settings" + imageVector = Icons.Default.Info, + contentDescription = "About" ) } } 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 deleted file mode 100644 index 77afa7f..0000000 --- a/app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/NetworkModeConfigActivity.kt +++ /dev/null @@ -1,220 +0,0 @@ -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(ExperimentalMaterial3ExpressiveApi::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(), - shapes = ButtonDefaults.shapes() - ) { - 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 deleted file mode 100644 index 91c3162..0000000 --- a/app/src/main/java/com/supernova/networkswitch/presentation/ui/activity/SettingsActivity.kt +++ /dev/null @@ -1,369 +0,0 @@ -package com.supernova.networkswitch.presentation.ui.activity - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.viewModels -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -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.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Error -import androidx.compose.material.icons.filled.Refresh -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.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.supernova.networkswitch.domain.model.CompatibilityState -import com.supernova.networkswitch.domain.model.ControlMethod -import com.supernova.networkswitch.presentation.theme.NetworkSwitchTheme -import com.supernova.networkswitch.presentation.viewmodel.SettingsViewModel -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class SettingsActivity : ComponentActivity() { - - private val viewModel: SettingsViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContent { - NetworkSwitchTheme { - SettingsScreen( - viewModel = viewModel, - onBackClick = { finish() } - ) - } - } - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun SettingsScreen( - viewModel: SettingsViewModel, - onBackClick: () -> Unit -) { - val controlMethod by viewModel.controlMethod.collectAsState() - - Scaffold( - topBar = { - TopAppBar( - title = { Text("Settings") }, - 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) - ) { - // Control Method Selection - ControlMethodCard( - selectedMethod = controlMethod, - onMethodSelected = { viewModel.updateControlMethod(it) }, - rootCompatibility = viewModel.rootCompatibility, - shizukuCompatibility = viewModel.shizukuCompatibility, - onRetryClick = { viewModel.retryCompatibilityCheck() } - ) - - // About Section - AboutCard() - } - } -} - -@Composable -private fun ControlMethodCard( - selectedMethod: ControlMethod, - onMethodSelected: (ControlMethod) -> Unit, - rootCompatibility: CompatibilityState, - shizukuCompatibility: CompatibilityState, - onRetryClick: () -> Unit -) { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Control Method", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - - IconButton(onClick = onRetryClick) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = "Refresh compatibility" - ) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "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.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(modifier = Modifier.height(16.dp)) - - // Root Method Option - Row( - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = selectedMethod == ControlMethod.ROOT, - onClick = { onMethodSelected(ControlMethod.ROOT) } - ) - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedMethod == ControlMethod.ROOT, - onClick = { onMethodSelected(ControlMethod.ROOT) } - ) - Spacer(modifier = Modifier.width(8.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = "Root Method", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium - ) - Text( - text = "Requires rooted device with root access granted", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - // Compatibility status indicator - when (rootCompatibility) { - 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 = "Error", - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(20.dp) - ) - } - } - } - - // Shizuku Method Option - Row( - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = selectedMethod == ControlMethod.SHIZUKU, - onClick = { onMethodSelected(ControlMethod.SHIZUKU) } - ) - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedMethod == ControlMethod.SHIZUKU, - onClick = { onMethodSelected(ControlMethod.SHIZUKU) } - ) - Spacer(modifier = Modifier.width(8.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = "Shizuku Method", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium - ) - Text( - text = "Works with non-rooted devices using Shizuku service", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - // Compatibility status indicator - when (shizukuCompatibility) { - 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 -private fun AboutCard() { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text( - text = "About", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "Source Code", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium - ) - - Spacer(modifier = Modifier.height(8.dp)) - - LinkItem( - title = "NetworkSwitch", - subtitle = "https://github.com/aunchagaonkar/NetworkSwitch", - link = "https://github.com/aunchagaonkar/NetworkSwitch" - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Text( - text = "Open Source Licenses", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium - ) - - Spacer(modifier = Modifier.height(8.dp)) - - LinkItem( - title = "Shizuku", - subtitle = "Apache License 2.0\nhttps://github.com/RikkaApps/Shizuku", - link = "https://github.com/RikkaApps/Shizuku" - ) - - LinkItem( - title = "libsu", - subtitle = "Apache License 2.0\nhttps://github.com/topjohnwu/libsu", - link = "https://github.com/topjohnwu/libsu" - ) - - LinkItem( - title = "Android Jetpack", - subtitle = "Apache License 2.0\nhttps://android.googlesource.com/platform/frameworks/support", - link = "https://android.googlesource.com/platform/frameworks/support" - ) - - LinkItem( - title = "Kotlin", - subtitle = "Apache License 2.0\nhttps://github.com/JetBrains/kotlin", - link = "https://github.com/JetBrains/kotlin" - ) - } - } -} - -@Composable -private fun LinkItem( - title: String, - subtitle: String, - link: String -) { - val context = LocalContext.current - - Column( - modifier = Modifier - .fillMaxWidth() - .clickable { - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link))) - } - .padding(vertical = 8.dp) - ) { - Text( - text = title, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = subtitle, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } -} diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/AboutCard.kt b/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/AboutCard.kt new file mode 100644 index 0000000..7c4df82 --- /dev/null +++ b/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/AboutCard.kt @@ -0,0 +1,119 @@ +package com.supernova.networkswitch.presentation.ui.composable + +import android.content.Intent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Code +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.Card +import androidx.compose.material3.DividerDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri + +@Composable +fun AboutCard() { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + Text( + text = "About", + style = MaterialTheme.typography.headlineSmall + ) + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = "Source Code", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp) + ) + Card(modifier = Modifier.fillMaxWidth()) { + LinkItem( + title = "NetworkSwitch", + subtitle = "https://github.com/aunchagaonkar/NetworkSwitch", + link = "https://github.com/aunchagaonkar/NetworkSwitch", + icon = Icons.Outlined.Code + ) + } + } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = "Open Source Licenses", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp) + ) + + val licenses = listOf( + Triple("Shizuku", "Apache License 2.0\nhttps://github.com/RikkaApps/Shizuku", "https://github.com/RikkaApps/Shizuku"), + Triple("libsu", "Apache License 2.0\nhttps://github.com/topjohnwu/libsu", "https://github.com/topjohnwu/libsu"), + Triple("Android Jetpack", "Apache License 2.0\nhttps://android.googlesource.com/platform/frameworks/support", "https://android.googlesource.com/platform/frameworks/support"), + Triple("Kotlin", "Apache License 2.0\nhttps://github.com/JetBrains/kotlin", "https://github.com/JetBrains/kotlin") + ) + + Card(modifier = Modifier.fillMaxWidth()) { + Column { + licenses.forEachIndexed { index, item -> + LinkItem( + title = item.first, + subtitle = item.second, + link = item.third, + icon = Icons.Outlined.Info + ) + if (index < licenses.lastIndex) { + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + thickness = DividerDefaults.Thickness, + color = DividerDefaults.color + ) + } + } + } + } + } + } +} + +@Composable +private fun LinkItem( + title: String, + subtitle: String, + link: String, + icon: ImageVector +) { + val context = LocalContext.current + ListItem( + headlineContent = { + Text( + text = title, + color = MaterialTheme.colorScheme.primary + ) + }, + supportingContent = { Text(text = subtitle) }, + leadingContent = { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + modifier = Modifier.clickable { + context.startActivity(Intent(Intent.ACTION_VIEW, link.toUri())) + } + ) +} 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 index 4df92a6..8903552 100644 --- 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 @@ -1,9 +1,10 @@ package com.supernova.networkswitch.presentation.ui.composable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll 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 @@ -12,66 +13,52 @@ import com.supernova.networkswitch.domain.model.NetworkMode @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun NetworkModeSelector( - label: String, selectedMode: NetworkMode, onModeSelected: (NetworkMode) -> Unit, - modifier: Modifier = Modifier, - availableModes: List = NetworkMode.values().toList() + availableModes: List = NetworkMode.entries, ) { var expanded by remember { mutableStateOf(false) } - - Card(modifier = modifier.fillMaxWidth()) { - Column( + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded } + ) { + OutlinedTextField( + value = selectedMode.displayName, + onValueChange = { }, + readOnly = true, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) + }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } ) { - Text( - text = label, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(8.dp)) - - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded } + Column( + modifier = Modifier + .heightIn(max = 300.dp) + .verticalScroll(rememberScrollState()) ) { - 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 - } - ) - } + availableModes.forEach { mode -> + DropdownMenuItem( + text = { + Text( + text = mode.displayName, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + }, + onClick = { + onModeSelected(mode) + expanded = false + } + ) } } } 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 00c907c..6009c52 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 @@ -1,14 +1,22 @@ package com.supernova.networkswitch.presentation.ui.composable +import android.app.StatusBarManager +import android.content.ComponentName +import android.content.Context +import android.graphics.drawable.Icon +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -16,6 +24,7 @@ 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.service.NetworkTileService private fun ControlMethod.displayName() = if (this == ControlMethod.SHIZUKU) "Shizuku" else "Root" @@ -176,32 +185,118 @@ fun NetworkToggleCard( } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun QuickSettingsHintCard(modifier: Modifier = Modifier) { - Card( - modifier = modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text( - text = "💡 Pro Tip", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) +fun QuickSettingsHintCard( + modifier: Modifier = Modifier +) { + val context = LocalContext.current + val isTileAdded by NetworkTileService.isTileAdded.collectAsState() + var hasTriedAutoAdd by remember { mutableStateOf(false) } + var showAddButton by remember { mutableStateOf(false) } + + // Auto-add tile on first composition + LaunchedEffect(Unit) { + if (!hasTriedAutoAdd && !isTileAdded) { + hasTriedAutoAdd = true + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val success = requestAddTileToQuickSettings(context) + showAddButton = !success + } else { + showAddButton = true + } + } + } - Spacer(modifier = Modifier.height(8.dp)) + // Update showAddButton when tile status changes + LaunchedEffect(isTileAdded) { + if (isTileAdded) { + showAddButton = false + } + } - Text( - 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 + if (isTileAdded || showAddButton) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = if (isTileAdded) "✅ Quick Settings" else "💡 Quick Settings", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = if (isTileAdded) { + "Great! You can now switch network modes directly from your Quick Settings panel. Just pull down your notification panel and tap the \"Network Switch\" tile." + } else { + "Add the \"Network Switch\" tile to your Quick Settings for instant network switching from anywhere on your device." + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + if (!isTileAdded && showAddButton) { + Spacer(modifier = Modifier.height(16.dp)) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Button( + onClick = { + val success = requestAddTileToQuickSettings(context) + if (success) showAddButton = false + }, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Add to Quick Settings") + } + } else { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Pull down your notification panel → Tap the pencil/edit icon → Find \"Network Switch\" → Drag it to your Quick Settings", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + } + } } } } + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +private fun requestAddTileToQuickSettings(context: Context): Boolean { + return try { + val statusBarManager = context.getSystemService(StatusBarManager::class.java) + val componentName = ComponentName(context, NetworkTileService::class.java) + + statusBarManager?.requestAddTileService( + componentName, + "Network Switch", + Icon.createWithResource(context, com.supernova.networkswitch.R.drawable.ic_5g_big), + { runnable -> runnable.run() }, + { result -> + // -> STATUS_BAR_MANAGER_TILE_ADDED or STATUS_BAR_MANAGER_TILE_NOT_ADDED + } + ) + true + } catch (e: Exception) { + e.printStackTrace() + false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/SettingsBottomSheet.kt b/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/SettingsBottomSheet.kt new file mode 100644 index 0000000..83eada3 --- /dev/null +++ b/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/SettingsBottomSheet.kt @@ -0,0 +1,424 @@ +package com.supernova.networkswitch.presentation.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +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.ArrowCircleDown +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.text.font.FontWeight +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.domain.model.ToggleModeConfig + +@Composable +fun SettingsBottomSheet( + // Settings properties + selectedControlMethod: ControlMethod, + onControlMethodSelected: (ControlMethod) -> Unit, + rootCompatibility: CompatibilityState, + shizukuCompatibility: CompatibilityState, + onRetryCompatibilityClick: () -> Unit, + + // Network Mode Config properties + currentConfig: ToggleModeConfig, + onModeASelected: (NetworkMode) -> Unit, + onModeBSelected: (NetworkMode) -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + ControlMethodCard( + selectedMethod = selectedControlMethod, + onMethodSelected = onControlMethodSelected, + rootCompatibility = rootCompatibility, + shizukuCompatibility = shizukuCompatibility, + onRetryClick = onRetryCompatibilityClick + ) + + NetworkModeConfigurationCard( + currentConfig = currentConfig, + onModeASelected = onModeASelected, + onModeBSelected = onModeBSelected + ) + Spacer(modifier = Modifier.height(8.dp)) + } +} + +@Composable +private fun ControlMethodCard( + selectedMethod: ControlMethod, + onMethodSelected: (ControlMethod) -> Unit, + rootCompatibility: CompatibilityState, + shizukuCompatibility: CompatibilityState, + onRetryClick: () -> Unit +) { + var showTooltip by remember { mutableStateOf(false) } + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "Control Method", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 16.dp) + ) + Box { + IconButton(onClick = { showTooltip = true }) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = "More info", + modifier = Modifier.size(16.dp) + ) + } + DropdownMenu( + expanded = showTooltip, + onDismissRequest = { showTooltip = false } + ) { + Text( + text = "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.", + modifier = Modifier + .padding(8.dp) + .width(300.dp) + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + IconButton(onClick = onRetryClick) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Refresh compatibility" + ) + } + } + + Card( + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) { + // Root Method Option + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = selectedMethod == ControlMethod.ROOT, + onClick = { onMethodSelected(ControlMethod.ROOT) } + ) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Root Method", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium + ) + Text( + text = "Requires rooted device with root access granted", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + // Compatibility status indicator + when (rootCompatibility) { + 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 = "Error", + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(20.dp) + ) + } + } + + RadioButton( + selected = selectedMethod == ControlMethod.ROOT, + onClick = { onMethodSelected(ControlMethod.ROOT) } + ) + } + + // Shizuku Method Option + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = selectedMethod == ControlMethod.SHIZUKU, + onClick = { onMethodSelected(ControlMethod.SHIZUKU) } + ) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Shizuku Method", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium + ) + Text( + text = "Works with non-rooted devices using Shizuku service", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Spacer(modifier = Modifier.width(8.dp)) + // Compatibility status indicator + when (shizukuCompatibility) { + 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) + ) + } + } + RadioButton( + selected = selectedMethod == ControlMethod.SHIZUKU, + onClick = { onMethodSelected(ControlMethod.SHIZUKU) } + ) + } + } + } + } +} + +@Composable +private fun NetworkModeConfigurationCard( + currentConfig: ToggleModeConfig, + onModeASelected: (NetworkMode) -> Unit, + onModeBSelected: (NetworkMode) -> Unit +) { + var showTooltip by remember { mutableStateOf(false) } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "Configure Network Modes", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 16.dp) + ) + Box { + IconButton(onClick = { showTooltip = true }) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = "More info", + modifier = Modifier.size(16.dp) + ) + } + DropdownMenu( + expanded = showTooltip, + onDismissRequest = { showTooltip = false } + ) { + Text( + text = "Set up the two network modes that the toggle will switch between. Changes are saved automatically.", + modifier = Modifier + .padding(8.dp) + .width(300.dp) + ) + } + } + } + Card( + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + NetworkModeSelector( + selectedMode = currentConfig.modeA, + onModeSelected = onModeASelected + ) + + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + ) + + Icon( + painter = rememberVectorPainter(image = Icons.Default.ArrowCircleDown), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + contentDescription = null, + modifier = Modifier.align( + Alignment.CenterHorizontally + ) + ) + + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + ) + + NetworkModeSelector( + selectedMode = currentConfig.modeB, + onModeSelected = onModeBSelected + ) + } + } + + // 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 + ) + } + } + + // 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 + ) + } + } + } + } +} 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 0fa943d..c1bfd0a 100644 --- a/app/src/main/java/com/supernova/networkswitch/service/NetworkTileService.kt +++ b/app/src/main/java/com/supernova/networkswitch/service/NetworkTileService.kt @@ -3,7 +3,6 @@ package com.supernova.networkswitch.service 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.model.NetworkMode import com.supernova.networkswitch.domain.model.ToggleModeConfig import com.supernova.networkswitch.domain.usecase.GetCurrentNetworkModeUseCase @@ -12,6 +11,9 @@ import com.supernova.networkswitch.domain.usecase.GetToggleModeConfigUseCase import com.supernova.networkswitch.domain.repository.PreferencesRepository import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject @AndroidEntryPoint @@ -34,8 +36,14 @@ class NetworkTileService : TileService() { private var currentNetworkMode: NetworkMode? = null private var toggleConfig: ToggleModeConfig? = null + companion object { + private val _isTileAdded = MutableStateFlow(false) + val isTileAdded: StateFlow = _isTileAdded.asStateFlow() + } + override fun onStartListening() { super.onStartListening() + _isTileAdded.value = true serviceScope.launch { try { // Observe toggle configuration changes @@ -51,6 +59,7 @@ class NetworkTileService : TileService() { override fun onStopListening() { super.onStopListening() + _isTileAdded.value = false // Clean up any ongoing operations when tile becomes inactive } @@ -105,7 +114,7 @@ class NetworkTileService : TileService() { val currentMode = config.getCurrentMode() val nextMode = config.getNextMode() label = currentMode.displayName - subtitle = "${nextMode.displayName}" + subtitle = nextMode.displayName } else { state = Tile.STATE_INACTIVE label = "Network Mode" @@ -120,6 +129,7 @@ class NetworkTileService : TileService() { override fun onDestroy() { super.onDestroy() + _isTileAdded.value = false serviceScope.cancel() } } From ac6d1c3806cd6bf2516c0c4c344c90a3058d3c70 Mon Sep 17 00:00:00 2001 From: s4tyendra Date: Thu, 2 Oct 2025 19:14:27 +0530 Subject: [PATCH 5/5] feat: Add option to hide the launcher icon This commit introduces a new feature allowing users to hide the app's icon from the launcher. This is useful for users who primarily interact with the app via its Quick Settings tile. The launcher icon's visibility can now be toggled from a new "App Settings" card within the settings bottom sheet. For the change to take effect, the user needs to relaunch the app. Key changes include: * **Settings UI**: Added a `Switch` in the `SettingsBottomSheet` to control the `hideLauncherIcon` preference. * **State Management**: Integrated `hideLauncherIcon` state flow into `SettingsViewModel`, `PreferencesRepository`, and `PreferencesDataSource`. * **Launcher Icon Control**: * Introduced a `LauncherIcon` object to dynamically enable or disable the launcher activity alias. * Added an `activity-alias` in `AndroidManifest.xml` for the main launcher activity to allow it to be toggled. * **Application Logic**: The `NetworkSwitchApplication` class now observes the preference and updates the launcher icon's enabled state on app startup. --- app/build.gradle.kts | 38 +++++++------ app/src/main/AndroidManifest.xml | 35 +++++------- .../networkswitch/NetworkSwitchApplication.kt | 29 +++++++++- .../repository/PreferencesRepositoryImpl.kt | 20 +++++-- .../data/source/PreferencesDataSource.kt | 42 +++++++++----- .../domain/repository/Repositories.kt | 26 ++++++--- .../presentation/LauncherIcon.kt | 33 +++++++++++ .../presentation/ui/activity/MainActivity.kt | 21 ++++--- .../ui/composable/SettingsBottomSheet.kt | 55 ++++++++++++++++++- .../viewmodel/SettingsViewModel.kt | 31 ++++++++--- 10 files changed, 244 insertions(+), 86 deletions(-) create mode 100644 app/src/main/java/com/supernova/networkswitch/presentation/LauncherIcon.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1b3132a..701610d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,11 +18,11 @@ android { versionName = "1.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - + vectorDrawables { useSupportLibrary = true } - + resourceConfigurations += listOf("en", "xxhdpi") } @@ -39,7 +39,7 @@ android { isJniDebuggable = false isPseudoLocalesEnabled = false } - + debug { isMinifyEnabled = false isDebuggable = true @@ -47,12 +47,12 @@ android { versionNameSuffix = "-debug" } } - + compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - + kotlinOptions { jvmTarget = "11" freeCompilerArgs += listOf( @@ -63,17 +63,17 @@ android { "-Xno-receiver-assertions" ) } - + buildFeatures { compose = true aidl = true buildConfig = true } - + composeOptions { kotlinCompilerExtensionVersion = libs.versions.compose.get() } - + testOptions { unitTests { isReturnDefaultValues = true @@ -81,7 +81,7 @@ android { } animationsDisabled = true } - + packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" @@ -89,12 +89,16 @@ android { excludes += "/META-INF/maven/**" excludes += "/androidsupportmultidexversion.txt" } - + jniLibs { useLegacyPackaging = false } } - + + lint { + disable += "NullSafeMutableLiveData" + } + // Bundle configuration for APK size optimization bundle { language { @@ -113,18 +117,18 @@ dependencies { implementation(libs.androidx.foundation) compileOnly(project(":hiddenapi")) - + // Root & Shizuku implementation(libs.libsu.core) implementation(libs.libsu.service) implementation(libs.shizuku.api) implementation(libs.shizuku.provider) - + // Core Android implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) - + // Compose implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) @@ -132,15 +136,15 @@ dependencies { implementation(libs.androidx.material3) implementation(libs.androidx.material.icons.extended) implementation(libs.androidx.lifecycle.viewmodel.compose) - + // Dependency Injection implementation(libs.hilt.android) implementation(libs.hilt.navigation.compose) kapt(libs.hilt.compiler) - + // Data Storage implementation(libs.androidx.datastore.preferences) - + // Testing testImplementation(libs.junit) testImplementation(libs.mockk) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e818558..a3bbb87 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,33 +13,26 @@ android:supportsRtl="true" android:theme="@style/Theme.NetworkSwitch" android:enableOnBackInvokedCallback="true" - tools:targetApi="31"> - + tools:targetApi="33"> + + + + - - - - - - + - + - + \ No newline at end of file diff --git a/app/src/main/java/com/supernova/networkswitch/NetworkSwitchApplication.kt b/app/src/main/java/com/supernova/networkswitch/NetworkSwitchApplication.kt index 31f5406..7f71998 100644 --- a/app/src/main/java/com/supernova/networkswitch/NetworkSwitchApplication.kt +++ b/app/src/main/java/com/supernova/networkswitch/NetworkSwitchApplication.kt @@ -1,7 +1,34 @@ package com.supernova.networkswitch import android.app.Application +import com.supernova.networkswitch.data.source.PreferencesDataSource +import com.supernova.networkswitch.presentation.LauncherIcon import dagger.hilt.android.HiltAndroidApp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject @HiltAndroidApp -class NetworkSwitchApplication : Application() +class NetworkSwitchApplication : Application() { + + @Inject + lateinit var preferencesDataSource: PreferencesDataSource + + private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + override fun onCreate() { + super.onCreate() + observeLauncherIconState() + } + + private fun observeLauncherIconState() { + preferencesDataSource.observeHideLauncherIcon() + .onEach { hide -> + LauncherIcon.setEnabled(this, !hide) + } + .launchIn(applicationScope) + } +} \ No newline at end of file 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 7f84c4d..46f26f6 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 @@ -15,28 +15,36 @@ import javax.inject.Singleton class PreferencesRepositoryImpl @Inject constructor( private val preferencesDataSource: PreferencesDataSource ) : PreferencesRepository { - + override suspend fun getControlMethod(): ControlMethod { return preferencesDataSource.getControlMethod() } - + override suspend fun setControlMethod(method: ControlMethod) { preferencesDataSource.setControlMethod(method) } - + 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() } + + override fun observeHideLauncherIcon(): Flow { + return preferencesDataSource.observeHideLauncherIcon() + } + + override suspend fun setHideLauncherIcon(hide: Boolean) { + preferencesDataSource.setHideLauncherIcon(hide) + } } 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 e8d8f02..c75bc3e 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 @@ -19,20 +19,34 @@ import javax.inject.Singleton class PreferencesDataSource @Inject constructor( private val dataStore: DataStore ) { - + companion object { + private val HIDE_LAUNCHER_ICON_KEY = booleanPreferencesKey("hide_launcher_icon") 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_HIDE_LAUNCHER_ICON = false 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 } - + + suspend fun setHideLauncherIcon(hide: Boolean) { + dataStore.edit { preferences -> + preferences[HIDE_LAUNCHER_ICON_KEY] = hide + } + } + + fun observeHideLauncherIcon(): Flow { + return dataStore.data.map { preferences -> + preferences[HIDE_LAUNCHER_ICON_KEY] ?: DEFAULT_HIDE_LAUNCHER_ICON + } + } + private fun parseControlMethod(methodString: String?): ControlMethod { return try { ControlMethod.valueOf(methodString ?: DEFAULT_CONTROL_METHOD) @@ -40,38 +54,38 @@ class PreferencesDataSource @Inject constructor( ControlMethod.SHIZUKU } } - + suspend fun getControlMethod(): ControlMethod { return dataStore.data.map { preferences -> parseControlMethod(preferences[CONTROL_METHOD_KEY]) }.first() } - + suspend fun setControlMethod(method: ControlMethod) { dataStore.edit { preferences -> preferences[CONTROL_METHOD_KEY] = method.name } } - + fun observeControlMethod(): Flow { return dataStore.data.map { preferences -> 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 @@ -79,16 +93,16 @@ class PreferencesDataSource @Inject constructor( 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/domain/repository/Repositories.kt b/app/src/main/java/com/supernova/networkswitch/domain/repository/Repositories.kt index 3d36256..937b3f5 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 @@ -14,17 +14,17 @@ interface NetworkControlRepository { * Check if network control is compatible with current device/method */ suspend fun checkCompatibility(method: ControlMethod): CompatibilityState - + /** * Get current network mode */ suspend fun getCurrentNetworkMode(subId: Int): NetworkMode? - + /** * Set network mode */ suspend fun setNetworkMode(subId: Int, mode: NetworkMode): Result - + /** * Reset connections - useful when switching control methods */ @@ -44,29 +44,39 @@ interface PreferencesRepository { * Get preferred control method */ suspend fun getControlMethod(): ControlMethod - + /** * Set preferred control method */ suspend fun setControlMethod(method: ControlMethod) - + /** * 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 + + /** + * Observe hide launcher icon changes + */ + fun observeHideLauncherIcon(): Flow + + /** + * Set hide launcher icon + */ + suspend fun setHideLauncherIcon(hide: Boolean) } diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/LauncherIcon.kt b/app/src/main/java/com/supernova/networkswitch/presentation/LauncherIcon.kt new file mode 100644 index 0000000..afb9cf4 --- /dev/null +++ b/app/src/main/java/com/supernova/networkswitch/presentation/LauncherIcon.kt @@ -0,0 +1,33 @@ +package com.supernova.networkswitch.presentation + +import android.content.Context +import android.content.pm.PackageManager +import com.supernova.networkswitch.BuildConfig + +object LauncherIcon { + + private val LAUNCHER_ACTIVITY_ALIAS = if (BuildConfig.DEBUG) { + "com.supernova.networkswitch.debug.presentation.ui.activity.Launcher" + } else { + "com.supernova.networkswitch.presentation.ui.activity.Launcher" + } + + fun setEnabled(context: Context, enabled: Boolean) { + val componentName = android.content.ComponentName( + context, + LAUNCHER_ACTIVITY_ALIAS + ) + + val newState = if (enabled) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } else { + PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + + context.packageManager.setComponentEnabledSetting( + componentName, + newState, + PackageManager.DONT_KILL_APP + ) + } +} \ No newline at end of file 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 b65a690..18a73cb 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 @@ -33,7 +33,7 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : ComponentActivity() { - + private val viewModel: MainViewModel by viewModels() private val settingsViewModel: SettingsViewModel by viewModels() private val networkModeConfigViewModel: NetworkModeConfigViewModel by viewModels() @@ -51,7 +51,7 @@ class MainActivity : ComponentActivity() { } } } - + override fun onResume() { super.onResume() viewModel.refreshAllData() @@ -73,6 +73,7 @@ private fun MainScreen( // Settings state val controlMethod by settingsViewModel.controlMethod.collectAsState() + val hideLauncherIcon by settingsViewModel.hideLauncherIcon.collectAsState() val currentConfig by networkModeConfigViewModel.currentConfig.collectAsState() if (showAboutBottomSheet) { @@ -97,14 +98,16 @@ private fun MainScreen( shizukuCompatibility = settingsViewModel.shizukuCompatibility, onRetryCompatibilityClick = { settingsViewModel.retryCompatibilityCheck() }, currentConfig = currentConfig, - onModeASelected = { mode -> - networkModeConfigViewModel.updateModeA(mode) + onModeASelected = { + networkModeConfigViewModel.updateModeA(it) networkModeConfigViewModel.saveConfiguration() }, - onModeBSelected = { mode -> - networkModeConfigViewModel.updateModeB(mode) + onModeBSelected = { + networkModeConfigViewModel.updateModeB(it) networkModeConfigViewModel.saveConfiguration() - } + }, + hideLauncherIcon = hideLauncherIcon, + onHideLauncherIconChanged = { settingsViewModel.updateHideLauncherIcon(it) } ) } } @@ -156,7 +159,7 @@ private fun MainScreen( currentControlMethod = viewModel.selectedMethod, onRetryClick = { viewModel.retryCompatibilityCheck() } ) - + // Network Toggle Card (show if compatible) if (compatibilityState is CompatibilityState.Compatible) { NetworkToggleCard( @@ -166,7 +169,7 @@ private fun MainScreen( onToggleClick = { viewModel.toggleNetworkMode() } ) } - + // Quick Settings Tip Card QuickSettingsHintCard() } diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/SettingsBottomSheet.kt b/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/SettingsBottomSheet.kt index 83eada3..87fdde2 100644 --- a/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/SettingsBottomSheet.kt +++ b/app/src/main/java/com/supernova/networkswitch/presentation/ui/composable/SettingsBottomSheet.kt @@ -27,6 +27,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -55,7 +56,11 @@ fun SettingsBottomSheet( // Network Mode Config properties currentConfig: ToggleModeConfig, onModeASelected: (NetworkMode) -> Unit, - onModeBSelected: (NetworkMode) -> Unit + onModeBSelected: (NetworkMode) -> Unit, + + // App settings + hideLauncherIcon: Boolean, + onHideLauncherIconChanged: (Boolean) -> Unit ) { Column( modifier = Modifier @@ -77,10 +82,58 @@ fun SettingsBottomSheet( onModeASelected = onModeASelected, onModeBSelected = onModeBSelected ) + + AppSettingsCard( + hideLauncherIcon = hideLauncherIcon, + onHideLauncherIconChanged = onHideLauncherIconChanged + ) Spacer(modifier = Modifier.height(8.dp)) } } +@Composable +private fun AppSettingsCard( + hideLauncherIcon: Boolean, + onHideLauncherIconChanged: (Boolean) -> Unit +) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = "App Settings", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 16.dp) + ) + Card( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Hide launcher icon", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium + ) + Text( + text = "Relaunch the app for changes to take effect", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Switch( + checked = hideLauncherIcon, + onCheckedChange = onHideLauncherIconChanged + ) + } + } + } +} + @Composable private fun ControlMethodCard( selectedMethod: ControlMethod, diff --git a/app/src/main/java/com/supernova/networkswitch/presentation/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/supernova/networkswitch/presentation/viewmodel/SettingsViewModel.kt index 147885b..d0196c6 100644 --- a/app/src/main/java/com/supernova/networkswitch/presentation/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/supernova/networkswitch/presentation/viewmodel/SettingsViewModel.kt @@ -24,44 +24,57 @@ class SettingsViewModel @Inject constructor( private val preferencesRepository: PreferencesRepository, private val networkControlRepository: NetworkControlRepository ) : ViewModel() { - + val controlMethod: StateFlow = preferencesRepository.observeControlMethod() .stateIn( scope = viewModelScope, started = kotlinx.coroutines.flow.SharingStarted.WhileSubscribed(5000), initialValue = ControlMethod.SHIZUKU ) - + + val hideLauncherIcon: StateFlow = preferencesRepository.observeHideLauncherIcon() + .stateIn( + scope = viewModelScope, + started = kotlinx.coroutines.flow.SharingStarted.WhileSubscribed(5000), + initialValue = false + ) + // Compatibility status for each method var rootCompatibility by mutableStateOf(CompatibilityState.Pending) private set - + var shizukuCompatibility by mutableStateOf(CompatibilityState.Pending) private set - + init { checkAllCompatibility() } - + fun updateControlMethod(method: ControlMethod) { viewModelScope.launch { preferencesRepository.setControlMethod(method) } } - + + fun updateHideLauncherIcon(hide: Boolean) { + viewModelScope.launch { + preferencesRepository.setHideLauncherIcon(hide) + } + } + fun retryCompatibilityCheck() { checkAllCompatibility() } - + private fun checkAllCompatibility() { viewModelScope.launch { rootCompatibility = CompatibilityState.Pending shizukuCompatibility = CompatibilityState.Pending - + // Check both methods in parallel val rootResult = async { networkControlRepository.checkCompatibility(ControlMethod.ROOT) } val shizukuResult = async { networkControlRepository.checkCompatibility(ControlMethod.SHIZUKU) } - + rootCompatibility = rootResult.await() shizukuCompatibility = shizukuResult.await() }