From f19c02b4d9e744784f20463a48942dec0d84ce1a Mon Sep 17 00:00:00 2001 From: retanar Date: Wed, 18 Dec 2024 11:57:25 +0200 Subject: [PATCH 1/8] Setup new screen --- .../featuremodule/homeImpl/HomeGraphEntry.kt | 11 ++++++ .../featuremodule/homeImpl/ui/HomeContract.kt | 1 + .../featuremodule/homeImpl/ui/HomeScreen.kt | 1 + .../com/featuremodule/homeImpl/ui/HomeVM.kt | 36 +++++++++---------- .../homeImpl/wifi/WifiContract.kt | 9 +++++ .../featuremodule/homeImpl/wifi/WifiScreen.kt | 8 +++++ .../com/featuremodule/homeImpl/wifi/WifiVM.kt | 13 +++++++ 7 files changed, 61 insertions(+), 18 deletions(-) create mode 100644 feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt create mode 100644 feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt create mode 100644 feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt index d52fb5d..beaf434 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt @@ -15,6 +15,7 @@ import com.featuremodule.homeImpl.camera.TakePhotoScreen import com.featuremodule.homeImpl.exoplayer.ExoplayerScreen import com.featuremodule.homeImpl.imageUpload.ImageUploadScreen import com.featuremodule.homeImpl.ui.HomeScreen +import com.featuremodule.homeImpl.wifi.WifiScreen fun NavGraphBuilder.registerHome() { composable(HomeDestination.ROUTE) { backStackEntry -> @@ -49,6 +50,10 @@ fun NavGraphBuilder.registerHome() { ?: "NONE" BarcodeResultScreen(barcode) } + + composable(InternalRoutes.WifiDestination.ROUTE) { + WifiScreen() + } } internal class InternalRoutes { @@ -87,4 +92,10 @@ internal class InternalRoutes { fun constructRoute(barcodeValue: String) = "barcode_result/$barcodeValue" } + + object WifiDestination { + const val ROUTE = "wifi" + + fun constructRoute() = ROUTE + } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt index 6cdbe4c..7a385b7 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt @@ -10,4 +10,5 @@ internal sealed interface Event : UiEvent { data object NavigateToExoplayer : Event data object NavigateToCamera : Event data object NavigateToBarcode : Event + data object NavigateToWifi : Event } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt index 460bd35..77b31cf 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt @@ -47,6 +47,7 @@ internal fun HomeScreen(route: String?, viewModel: HomeVM = hiltViewModel()) { GenericButton(text = "Exoplayer") { viewModel.postEvent(Event.NavigateToExoplayer) } GenericButton(text = "Camera") { viewModel.postEvent(Event.NavigateToCamera) } GenericButton(text = "Barcode") { viewModel.postEvent(Event.NavigateToBarcode) } + GenericButton(text = "Wifi") { viewModel.postEvent(Event.NavigateToWifi) } } } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt index af6c8bc..3e6699e 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt @@ -17,33 +17,33 @@ internal class HomeVM @Inject constructor( override fun initialState() = State override fun handleEvent(event: Event) { - when (event) { - Event.NavigateToFeatureA -> launch { - val randomInt = Random.nextInt(until = 10) - // Using `saveState = false` causes return to Feature A on Home item click - navManager.navigate(NavCommand.OpenNavBarRoute(NavBarItems.FeatureA.graphRoute)) - - navManager.navigate( - NavCommand.Forward(FeatureADestination.constructRoute(randomInt)), - ) - } + launch { + when (event) { + Event.NavigateToFeatureA -> { + val randomInt = Random.nextInt(until = 10) + // Using `saveState = false` causes return to Feature A on Home item click + navManager.navigate(NavCommand.OpenNavBarRoute(NavBarItems.FeatureA.graphRoute)) - Event.NavigateToExoplayer -> launch { - navManager.navigate( + navManager.navigate( + NavCommand.Forward(FeatureADestination.constructRoute(randomInt)), + ) + } + + Event.NavigateToExoplayer -> navManager.navigate( NavCommand.Forward(InternalRoutes.ExoplayerDestination.constructRoute()), ) - } - Event.NavigateToCamera -> launch { - navManager.navigate( + Event.NavigateToCamera -> navManager.navigate( NavCommand.Forward(InternalRoutes.ImageUploadDestination.constructRoute()), ) - } - Event.NavigateToBarcode -> launch { - navManager.navigate( + Event.NavigateToBarcode -> navManager.navigate( NavCommand.Forward(InternalRoutes.BarcodeCameraDestination.constructRoute()), ) + + Event.NavigateToWifi -> navManager.navigate( + NavCommand.Forward(InternalRoutes.WifiDestination.constructRoute()), + ) } } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt new file mode 100644 index 0000000..63057a8 --- /dev/null +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt @@ -0,0 +1,9 @@ +package com.featuremodule.homeImpl.wifi + +import com.featuremodule.core.ui.UiEvent +import com.featuremodule.core.ui.UiState + +internal class State : UiState + +internal sealed interface Event : UiEvent { +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt new file mode 100644 index 0000000..abf6880 --- /dev/null +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt @@ -0,0 +1,8 @@ +package com.featuremodule.homeImpl.wifi + +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel + +@Composable +internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt new file mode 100644 index 0000000..cf84a30 --- /dev/null +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt @@ -0,0 +1,13 @@ +package com.featuremodule.homeImpl.wifi + +import com.featuremodule.core.ui.BaseVM +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +internal class WifiVM @Inject constructor() : BaseVM() { + override fun initialState() = State() + + override fun handleEvent(event: Event) { + } +} From 178e2c3b053a148e2d220ae05f0cc737ad08ddba Mon Sep 17 00:00:00 2001 From: retanar Date: Thu, 19 Dec 2024 13:19:46 +0200 Subject: [PATCH 2/8] Basic scanning and UI --- feature/homeImpl/src/main/AndroidManifest.xml | 3 + .../homeImpl/wifi/WifiContract.kt | 15 ++- .../featuremodule/homeImpl/wifi/WifiScreen.kt | 103 ++++++++++++++++++ .../com/featuremodule/homeImpl/wifi/WifiVM.kt | 24 ++++ 4 files changed, 144 insertions(+), 1 deletion(-) diff --git a/feature/homeImpl/src/main/AndroidManifest.xml b/feature/homeImpl/src/main/AndroidManifest.xml index 15d7e8a..7dc382a 100644 --- a/feature/homeImpl/src/main/AndroidManifest.xml +++ b/feature/homeImpl/src/main/AndroidManifest.xml @@ -4,6 +4,9 @@ android:name="android.hardware.camera" android:required="false" /> + + + = emptyList(), +) : UiState + +internal data class NetworkState( + val ssid: String, + val bssid: String, + val bandGhz: String, + val channel: Int, + val channelWidthMhz: Int, + val level: Int, +) internal sealed interface Event : UiEvent { + data class WifiResultsScanned(val result: List) : Event } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt index abf6880..bea13e3 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt @@ -1,8 +1,111 @@ package com.featuremodule.homeImpl.wifi +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.wifi.WifiManager +import android.net.wifi.WifiManager.ScanResultsCallback +import android.os.Build +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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 androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle @Composable internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { + val context = LocalContext.current.applicationContext + val wifiManager = remember { context.getSystemService(Context.WIFI_SERVICE) as WifiManager } + val isWifiEnabled by remember { mutableStateOf(wifiManager.isWifiEnabled) } + + val state by viewModel.state.collectAsStateWithLifecycle() + + fun sendScanResults() { + try { + viewModel.postEvent(Event.WifiResultsScanned(wifiManager.scanResults)) + } catch (e: SecurityException) { + e.printStackTrace() + } + } + + DisposableEffect(context, wifiManager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val callback = object : ScanResultsCallback() { + override fun onScanResultsAvailable() { + sendScanResults() + } + } + wifiManager.registerScanResultsCallback( + ContextCompat.getMainExecutor(context), + callback, + ) + + onDispose { + wifiManager.unregisterScanResultsCallback(callback) + } + } else { + val wifiScanReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + sendScanResults() + } + } + val intentFilter = IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) + context.registerReceiver(wifiScanReceiver, intentFilter) + + onDispose { + context.unregisterReceiver(wifiScanReceiver) + } + } + } + + LaunchedEffect(isWifiEnabled) { + sendScanResults() + if (isWifiEnabled) { + wifiManager.startScan() + } + } + + LazyColumn { + if (!isWifiEnabled) { + item { + Text(text = "Wifi is not enabled") + } + } + items(items = state.wifiNetworks) { + WifiNetworkItem(state = it) + } + } +} + +@Composable +private fun WifiNetworkItem(state: NetworkState) { + Card { + Row( + modifier = Modifier.padding(all = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text(text = state.ssid, fontWeight = FontWeight.SemiBold, fontSize = 18.sp) + } + Text(text = state.level.toString()) + } + } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt index cf84a30..336f88d 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt @@ -1,5 +1,6 @@ package com.featuremodule.homeImpl.wifi +import android.net.wifi.ScanResult import com.featuremodule.core.ui.BaseVM import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -9,5 +10,28 @@ internal class WifiVM @Inject constructor() : BaseVM() { override fun initialState() = State() override fun handleEvent(event: Event) { + when (event) { + is Event.WifiResultsScanned -> setState { + copy(wifiNetworks = event.result.map { it.toNetworkState() }) + } + } } + + private fun ScanResult.toNetworkState() = NetworkState( + ssid = SSID, + bssid = BSSID, + bandGhz = "", + channel = ScanResult.convertFrequencyMhzToChannelIfSupported(frequency), + channelWidthMhz = when (channelWidth) { + ScanResult.CHANNEL_WIDTH_20MHZ -> 20 + ScanResult.CHANNEL_WIDTH_40MHZ -> 40 + ScanResult.CHANNEL_WIDTH_80MHZ, + ScanResult.CHANNEL_WIDTH_80MHZ_PLUS_MHZ -> 80 + + ScanResult.CHANNEL_WIDTH_160MHZ -> 160 + ScanResult.CHANNEL_WIDTH_320MHZ -> 320 + else -> -1 + }, + level = level, + ) } From 77aca47ea7e2b1e6abc111ce27c4f9370a7c0185 Mon Sep 17 00:00:00 2001 From: retanar Date: Fri, 20 Dec 2024 14:40:05 +0200 Subject: [PATCH 3/8] Wifi saving, suggesting and IoT connecting APIs --- feature/homeImpl/src/main/AndroidManifest.xml | 1 + .../homeImpl/wifi/WifiContract.kt | 6 ++ .../featuremodule/homeImpl/wifi/WifiScreen.kt | 79 ++++++++++++++++++- .../com/featuremodule/homeImpl/wifi/WifiVM.kt | 44 ++++++++++- 4 files changed, 126 insertions(+), 4 deletions(-) diff --git a/feature/homeImpl/src/main/AndroidManifest.xml b/feature/homeImpl/src/main/AndroidManifest.xml index 7dc382a..1839ec2 100644 --- a/feature/homeImpl/src/main/AndroidManifest.xml +++ b/feature/homeImpl/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + = emptyList(), + val wifiToConnect: NetworkRequest? = null, + val wifiSuggestions: ArrayList? = null, ) : UiState internal data class NetworkState( @@ -19,4 +23,6 @@ internal data class NetworkState( internal sealed interface Event : UiEvent { data class WifiResultsScanned(val result: List) : Event + data class SaveWifi(val network: NetworkState) : Event + data object ClearWifiEvents : Event } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt index bea13e3..c0178c3 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt @@ -4,9 +4,16 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.Network import android.net.wifi.WifiManager import android.net.wifi.WifiManager.ScanResultsCallback import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -34,6 +41,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { val context = LocalContext.current.applicationContext val wifiManager = remember { context.getSystemService(Context.WIFI_SERVICE) as WifiManager } + val connectivityManager = + remember { context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } val isWifiEnabled by remember { mutableStateOf(wifiManager.isWifiEnabled) } val state by viewModel.state.collectAsStateWithLifecycle() @@ -83,6 +92,69 @@ internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { } } + val launchAddWifi = rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ) { activityResult -> + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return@rememberLauncherForActivityResult + + val result = activityResult.data + ?.getIntegerArrayListExtra(Settings.EXTRA_WIFI_NETWORK_RESULT_LIST) + .orEmpty() + + Log.d( + "WIFI", + result.joinToString { + when (it) { + Settings.ADD_WIFI_RESULT_SUCCESS -> "ADD_WIFI_RESULT_SUCCESS" + Settings.ADD_WIFI_RESULT_ADD_OR_UPDATE_FAILED -> "ADD_WIFI_RESULT_ADD_OR_UPDATE_FAILED" + Settings.ADD_WIFI_RESULT_ALREADY_EXISTS -> "ADD_WIFI_RESULT_ALREADY_EXISTS" + else -> "OTHER" + } + }, + ) + + if (result.any { it == Settings.ADD_WIFI_RESULT_SUCCESS || it == Settings.ADD_WIFI_RESULT_ALREADY_EXISTS }) { + wifiManager.addNetworkSuggestions(state.wifiSuggestions.orEmpty()) + } + viewModel.postEvent(Event.ClearWifiEvents) + } + + LaunchedEffect(state.wifiSuggestions) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@LaunchedEffect + + val suggestions = state.wifiSuggestions + if (suggestions.isNullOrEmpty()) return@LaunchedEffect + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val bundle = Bundle().apply { + putParcelableArrayList(Settings.EXTRA_WIFI_NETWORK_LIST, suggestions) + } + launchAddWifi.launch(Intent(Settings.ACTION_WIFI_ADD_NETWORKS).putExtras(bundle)) + } else { + // Requires only API Q (29) + wifiManager.addNetworkSuggestions(suggestions) + viewModel.postEvent(Event.ClearWifiEvents) + } + } + + LaunchedEffect(state.wifiToConnect) { + if (state.wifiToConnect == null) return@LaunchedEffect + + connectivityManager.requestNetwork( + state.wifiToConnect!!, + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + Log.d("WIFI", "onAvailable") + } + + override fun onUnavailable() { + Log.d("WIFI", "onUnavailable") + } + }, + ) + viewModel.postEvent(Event.ClearWifiEvents) + } + LazyColumn { if (!isWifiEnabled) { item { @@ -90,20 +162,21 @@ internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { } } items(items = state.wifiNetworks) { - WifiNetworkItem(state = it) + WifiNetworkItem(state = it, onClick = { viewModel.postEvent(Event.SaveWifi(it)) }) } } } @Composable -private fun WifiNetworkItem(state: NetworkState) { - Card { +private fun WifiNetworkItem(state: NetworkState, onClick: () -> Unit) { + Card(onClick = onClick) { Row( modifier = Modifier.padding(all = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { Column(modifier = Modifier.weight(1f)) { Text(text = state.ssid, fontWeight = FontWeight.SemiBold, fontSize = 18.sp) + Text(text = state.bssid, fontWeight = FontWeight.Light) } Text(text = state.level.toString()) } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt index 336f88d..82649a9 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt @@ -1,6 +1,13 @@ package com.featuremodule.homeImpl.wifi +import android.net.MacAddress +import android.net.NetworkCapabilities +import android.net.NetworkRequest import android.net.wifi.ScanResult +import android.net.wifi.WifiNetworkSpecifier +import android.net.wifi.WifiNetworkSuggestion +import android.os.Build +import androidx.annotation.RequiresApi import com.featuremodule.core.ui.BaseVM import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -14,6 +21,12 @@ internal class WifiVM @Inject constructor() : BaseVM() { is Event.WifiResultsScanned -> setState { copy(wifiNetworks = event.result.map { it.toNetworkState() }) } + + is Event.SaveWifi -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + saveAndSuggestWifi(event.network) + } + + Event.ClearWifiEvents -> setState { copy(wifiToConnect = null, wifiSuggestions = null) } } } @@ -21,7 +34,11 @@ internal class WifiVM @Inject constructor() : BaseVM() { ssid = SSID, bssid = BSSID, bandGhz = "", - channel = ScanResult.convertFrequencyMhzToChannelIfSupported(frequency), + channel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + ScanResult.convertFrequencyMhzToChannelIfSupported(frequency) + } else { + -1 + }, channelWidthMhz = when (channelWidth) { ScanResult.CHANNEL_WIDTH_20MHZ -> 20 ScanResult.CHANNEL_WIDTH_40MHZ -> 40 @@ -34,4 +51,29 @@ internal class WifiVM @Inject constructor() : BaseVM() { }, level = level, ) + + @RequiresApi(Build.VERSION_CODES.Q) + private fun connectToIotWifi(network: NetworkState) { + val specifier = WifiNetworkSpecifier.Builder() + .setSsid(network.ssid) + .setBssid(MacAddress.fromString(network.bssid)) + .build() + val request = NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .setNetworkSpecifier(specifier) + .build() + setState { copy(wifiToConnect = request) } + } + + @RequiresApi(Build.VERSION_CODES.Q) + private fun saveAndSuggestWifi(network: NetworkState) { + val suggestions = arrayListOf( + WifiNetworkSuggestion.Builder() + .setSsid(network.ssid) + .setBssid(MacAddress.fromString(network.bssid)) + .build(), + ) + + setState { copy(wifiSuggestions = suggestions) } + } } From 5d69d6a28b5998c8c021ede9b8beb570620b7434 Mon Sep 17 00:00:00 2001 From: retanar Date: Fri, 20 Dec 2024 15:40:34 +0200 Subject: [PATCH 4/8] WifiItemMenu for separate functions --- .../homeImpl/wifi/WifiContract.kt | 1 + .../featuremodule/homeImpl/wifi/WifiScreen.kt | 45 ++++++++++++++++++- .../com/featuremodule/homeImpl/wifi/WifiVM.kt | 4 ++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt index 1f88a85..eaa833f 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt @@ -24,5 +24,6 @@ internal data class NetworkState( internal sealed interface Event : UiEvent { data class WifiResultsScanned(val result: List) : Event data class SaveWifi(val network: NetworkState) : Event + data class ConnectWifi(val network: NetworkState) : Event data object ClearWifiEvents : Event } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt index c0178c3..2088fc0 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt @@ -14,8 +14,10 @@ import android.provider.Settings import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -27,12 +29,14 @@ import androidx.compose.runtime.LaunchedEffect 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.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -155,6 +159,7 @@ internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { viewModel.postEvent(Event.ClearWifiEvents) } + var clickedWifiItem by remember { mutableStateOf(null) } LazyColumn { if (!isWifiEnabled) { item { @@ -162,9 +167,17 @@ internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { } } items(items = state.wifiNetworks) { - WifiNetworkItem(state = it, onClick = { viewModel.postEvent(Event.SaveWifi(it)) }) + WifiNetworkItem(state = it, onClick = { clickedWifiItem = it }) } } + + clickedWifiItem?.let { + WifiItemMenu( + onDismiss = { clickedWifiItem = null }, + onSave = { viewModel.postEvent(Event.SaveWifi(it)) }, + onConnect = { viewModel.postEvent(Event.ConnectWifi(it)) }, + ) + } } @Composable @@ -182,3 +195,33 @@ private fun WifiNetworkItem(state: NetworkState, onClick: () -> Unit) { } } } + +@Composable +private fun WifiItemMenu( + onDismiss: () -> Unit, + onSave: () -> Unit, + onConnect: () -> Unit, +) { + @Composable + fun Entry(text: String, onClick: () -> Unit) { + Text( + text = text, + modifier = Modifier + .fillMaxWidth() + .clickable { + onClick() + onDismiss() + } + .padding(all = 16.dp), + ) + } + + Dialog(onDismissRequest = onDismiss) { + Card { + Column { + Entry("Save this network") { onSave() } + Entry("Connect as IoT") { onConnect() } + } + } + } +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt index 82649a9..430c0b3 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt @@ -26,6 +26,10 @@ internal class WifiVM @Inject constructor() : BaseVM() { saveAndSuggestWifi(event.network) } + is Event.ConnectWifi -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + connectToIotWifi(event.network) + } + Event.ClearWifiEvents -> setState { copy(wifiToConnect = null, wifiSuggestions = null) } } } From f17680326541bbe9680a8f07865b2d3a6a02a613 Mon Sep 17 00:00:00 2001 From: retanar Date: Mon, 23 Dec 2024 11:22:28 +0200 Subject: [PATCH 5/8] UI update, copied ScanResult's utility functions into WifiUtils for compatibility with all SDKs --- .../featuremodule/core/util/WifiUtils.java | 173 ++++++++++++++++++ .../featuremodule/homeImpl/wifi/WifiScreen.kt | 27 ++- .../com/featuremodule/homeImpl/wifi/WifiVM.kt | 23 ++- 3 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 core/src/main/java/com/featuremodule/core/util/WifiUtils.java diff --git a/core/src/main/java/com/featuremodule/core/util/WifiUtils.java b/core/src/main/java/com/featuremodule/core/util/WifiUtils.java new file mode 100644 index 0000000..48d33e6 --- /dev/null +++ b/core/src/main/java/com/featuremodule/core/util/WifiUtils.java @@ -0,0 +1,173 @@ +package com.featuremodule.core.util; + +/** + * Copy of ScanResult utility functions unavailable on earlier SDKs + */ +public class WifiUtils { + /** + * The unspecified value. + */ + public final static int UNSPECIFIED = -1; + + /** + * 2.4 GHz band first channel number + */ + public static final int BAND_24_GHZ_FIRST_CH_NUM = 1; + /** + * 2.4 GHz band last channel number + */ + public static final int BAND_24_GHZ_LAST_CH_NUM = 14; + /** + * 2.4 GHz band frequency of first channel in MHz + */ + public static final int BAND_24_GHZ_START_FREQ_MHZ = 2412; + /** + * 2.4 GHz band frequency of last channel in MHz + */ + public static final int BAND_24_GHZ_END_FREQ_MHZ = 2484; + + /** + * 5 GHz band first channel number + */ + public static final int BAND_5_GHZ_FIRST_CH_NUM = 32; + /** + * 5 GHz band last channel number + */ + public static final int BAND_5_GHZ_LAST_CH_NUM = 177; + /** + * 5 GHz band frequency of first channel in MHz + */ + public static final int BAND_5_GHZ_START_FREQ_MHZ = 5160; + /** + * 5 GHz band frequency of last channel in MHz + */ + public static final int BAND_5_GHZ_END_FREQ_MHZ = 5885; + + /** + * 6 GHz band first channel number + */ + public static final int BAND_6_GHZ_FIRST_CH_NUM = 1; + /** + * 6 GHz band last channel number + */ + public static final int BAND_6_GHZ_LAST_CH_NUM = 233; + /** + * 6 GHz band frequency of first channel in MHz + */ + public static final int BAND_6_GHZ_START_FREQ_MHZ = 5955; + /** + * 6 GHz band frequency of last channel in MHz + */ + public static final int BAND_6_GHZ_END_FREQ_MHZ = 7115; + /** + * The center frequency of the first 6Ghz preferred scanning channel, as defined by + * IEEE802.11ax draft 7.0 section 26.17.2.3.3. + */ + public static final int BAND_6_GHZ_PSC_START_MHZ = 5975; + /** + * The number of MHz to increment in order to get the next 6Ghz preferred scanning channel + * as defined by IEEE802.11ax draft 7.0 section 26.17.2.3.3. + */ + public static final int BAND_6_GHZ_PSC_STEP_SIZE_MHZ = 80; + + /** + * 6 GHz band operating class 136 channel 2 center frequency in MHz + */ + public static final int BAND_6_GHZ_OP_CLASS_136_CH_2_FREQ_MHZ = 5935; + + /** + * 60 GHz band first channel number + */ + public static final int BAND_60_GHZ_FIRST_CH_NUM = 1; + /** + * 60 GHz band last channel number + */ + public static final int BAND_60_GHZ_LAST_CH_NUM = 6; + /** + * 60 GHz band frequency of first channel in MHz + */ + public static final int BAND_60_GHZ_START_FREQ_MHZ = 58320; + /** + * 60 GHz band frequency of last channel in MHz + */ + public static final int BAND_60_GHZ_END_FREQ_MHZ = 70200; + + /** + * Utility function to check if a frequency within 2.4 GHz band + * + * @param freqMhz frequency in MHz + * @return true if within 2.4GHz, false otherwise + */ + public static boolean is24GHz(int freqMhz) { + return freqMhz >= BAND_24_GHZ_START_FREQ_MHZ && freqMhz <= BAND_24_GHZ_END_FREQ_MHZ; + } + + /** + * Utility function to check if a frequency within 5 GHz band + * + * @param freqMhz frequency in MHz + * @return true if within 5GHz, false otherwise + */ + public static boolean is5GHz(int freqMhz) { + return freqMhz >= BAND_5_GHZ_START_FREQ_MHZ && freqMhz <= BAND_5_GHZ_END_FREQ_MHZ; + } + + /** + * Utility function to check if a frequency within 6 GHz band + * + * @return true if within 6GHz, false otherwise + */ + public static boolean is6GHz(int freqMhz) { + if (freqMhz == BAND_6_GHZ_OP_CLASS_136_CH_2_FREQ_MHZ) { + return true; + } + return (freqMhz >= BAND_6_GHZ_START_FREQ_MHZ && freqMhz <= BAND_6_GHZ_END_FREQ_MHZ); + } + + /** + * Utility function to check if a frequency is 6Ghz PSC channel. + * + * @return true if the frequency is 6GHz PSC, false otherwise + */ + public static boolean is6GHzPsc(int freqMhz) { + if (!is6GHz(freqMhz)) { + return false; + } + return (freqMhz - BAND_6_GHZ_PSC_START_MHZ) % BAND_6_GHZ_PSC_STEP_SIZE_MHZ == 0; + } + + /** + * Utility function to check if a frequency within 60 GHz band + * + * @return true if within 60GHz, false otherwise + */ + public static boolean is60GHz(int freqMhz) { + return freqMhz >= BAND_60_GHZ_START_FREQ_MHZ && freqMhz <= BAND_60_GHZ_END_FREQ_MHZ; + } + + /** + * Utility function to convert frequency in MHz to channel number. + * + * @param freqMhz frequency in MHz + * @return channel number associated with given frequency, {@link #UNSPECIFIED} if no match + */ + public static int convertFrequencyMhzToChannelIfSupported(int freqMhz) { + // Special case + if (freqMhz == 2484) { + return 14; + } else if (is24GHz(freqMhz)) { + return (freqMhz - BAND_24_GHZ_START_FREQ_MHZ) / 5 + BAND_24_GHZ_FIRST_CH_NUM; + } else if (is5GHz(freqMhz)) { + return ((freqMhz - BAND_5_GHZ_START_FREQ_MHZ) / 5) + BAND_5_GHZ_FIRST_CH_NUM; + } else if (is6GHz(freqMhz)) { + if (freqMhz == BAND_6_GHZ_OP_CLASS_136_CH_2_FREQ_MHZ) { + return 2; + } + return ((freqMhz - BAND_6_GHZ_START_FREQ_MHZ) / 5) + BAND_6_GHZ_FIRST_CH_NUM; + } else if (is60GHz(freqMhz)) { + return ((freqMhz - BAND_60_GHZ_START_FREQ_MHZ) / 2160) + BAND_60_GHZ_FIRST_CH_NUM; + } + + return UNSPECIFIED; + } +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt index 2088fc0..d97932b 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt @@ -15,6 +15,7 @@ import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -34,6 +35,7 @@ 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.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog @@ -182,16 +184,33 @@ internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { @Composable private fun WifiNetworkItem(state: NetworkState, onClick: () -> Unit) { - Card(onClick = onClick) { + Card(onClick = onClick, modifier = Modifier.padding(2.dp)) { Row( modifier = Modifier.padding(all = 16.dp), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Column(modifier = Modifier.weight(1f)) { - Text(text = state.ssid, fontWeight = FontWeight.SemiBold, fontSize = 18.sp) - Text(text = state.bssid, fontWeight = FontWeight.Light) + Text( + text = state.ssid, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (state.channel != -1) { + Text(text = "Channel ${state.channel}", fontSize = 14.sp) + } } - Text(text = state.level.toString()) + + if (state.bandGhz.isNotEmpty() && state.channelWidthMhz != -1) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = "${state.bandGhz} GHz") + Text(text = "${state.channelWidthMhz} MHz", fontSize = 14.sp) + } + } + + Text(text = "${state.level}") } } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt index 430c0b3..665565b 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt @@ -9,6 +9,7 @@ import android.net.wifi.WifiNetworkSuggestion import android.os.Build import androidx.annotation.RequiresApi import com.featuremodule.core.ui.BaseVM +import com.featuremodule.core.util.WifiUtils import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -19,7 +20,11 @@ internal class WifiVM @Inject constructor() : BaseVM() { override fun handleEvent(event: Event) { when (event) { is Event.WifiResultsScanned -> setState { - copy(wifiNetworks = event.result.map { it.toNetworkState() }) + copy( + wifiNetworks = event.result + .map { it.toNetworkState() } + .sortedByDescending { it.level }, + ) } is Event.SaveWifi -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -37,12 +42,8 @@ internal class WifiVM @Inject constructor() : BaseVM() { private fun ScanResult.toNetworkState() = NetworkState( ssid = SSID, bssid = BSSID, - bandGhz = "", - channel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - ScanResult.convertFrequencyMhzToChannelIfSupported(frequency) - } else { - -1 - }, + bandGhz = toBand(frequency), + channel = WifiUtils.convertFrequencyMhzToChannelIfSupported(frequency), channelWidthMhz = when (channelWidth) { ScanResult.CHANNEL_WIDTH_20MHZ -> 20 ScanResult.CHANNEL_WIDTH_40MHZ -> 40 @@ -80,4 +81,12 @@ internal class WifiVM @Inject constructor() : BaseVM() { setState { copy(wifiSuggestions = suggestions) } } + + private fun toBand(frequency: Int) = when { + WifiUtils.is24GHz(frequency) -> "2.4" + WifiUtils.is5GHz(frequency) -> "5" + WifiUtils.is6GHz(frequency) -> "6" + WifiUtils.is60GHz(frequency) -> "60" + else -> "" + } } From fae6e168c485dcb2e436563e8b9dc98c28cb6586 Mon Sep 17 00:00:00 2001 From: retanar Date: Tue, 24 Dec 2024 11:33:45 +0200 Subject: [PATCH 6/8] Refactoring, cleaning up WifiScreen, adding buttons to enable Location and Wifi from settings --- .../homeImpl/wifi/WifiContract.kt | 4 + .../featuremodule/homeImpl/wifi/WifiScreen.kt | 329 +++++++++++------- .../com/featuremodule/homeImpl/wifi/WifiVM.kt | 2 + 3 files changed, 210 insertions(+), 125 deletions(-) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt index eaa833f..21e5170 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt @@ -10,6 +10,8 @@ internal data class State( val wifiNetworks: List = emptyList(), val wifiToConnect: NetworkRequest? = null, val wifiSuggestions: ArrayList? = null, + val isLocationEnabled: Boolean = true, + val isWifiEnabled: Boolean = true, ) : UiState internal data class NetworkState( @@ -26,4 +28,6 @@ internal sealed interface Event : UiEvent { data class SaveWifi(val network: NetworkState) : Event data class ConnectWifi(val network: NetworkState) : Event data object ClearWifiEvents : Event + data class UpdateLocationEnabled(val enabled: Boolean) : Event + data class UpdateWifiEnabled(val enabled: Boolean) : Event } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt index d97932b..4185ee8 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt @@ -4,24 +4,27 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.location.LocationManager import android.net.ConnectivityManager -import android.net.Network +import android.net.NetworkRequest import android.net.wifi.WifiManager import android.net.wifi.WifiManager.ScanResultsCallback +import android.net.wifi.WifiNetworkSuggestion import android.os.Build import android.os.Bundle import android.provider.Settings -import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -34,25 +37,39 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.core.content.ContextCompat +import androidx.core.location.LocationManagerCompat import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.repeatOnLifecycle @Composable internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { - val context = LocalContext.current.applicationContext - val wifiManager = remember { context.getSystemService(Context.WIFI_SERVICE) as WifiManager } - val connectivityManager = - remember { context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } - val isWifiEnabled by remember { mutableStateOf(wifiManager.isWifiEnabled) } + val context = LocalContext.current + val wifiManager = remember { context.getSystemService(WifiManager::class.java) } val state by viewModel.state.collectAsStateWithLifecycle() + val lifecycle = LocalLifecycleOwner.current.lifecycle + val locationManager = remember { context.getSystemService(LocationManager::class.java) } + LaunchedEffect(Unit) { + lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel.postEvent( + Event.UpdateLocationEnabled( + LocationManagerCompat.isLocationEnabled(locationManager), + ), + ) + viewModel.postEvent(Event.UpdateWifiEnabled(wifiManager.isWifiEnabled)) + } + } + fun sendScanResults() { try { viewModel.postEvent(Event.WifiResultsScanned(wifiManager.scanResults)) @@ -61,113 +78,75 @@ internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { } } - DisposableEffect(context, wifiManager) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val callback = object : ScanResultsCallback() { - override fun onScanResultsAvailable() { - sendScanResults() - } - } - wifiManager.registerScanResultsCallback( - ContextCompat.getMainExecutor(context), - callback, - ) - - onDispose { - wifiManager.unregisterScanResultsCallback(callback) - } - } else { - val wifiScanReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - sendScanResults() - } - } - val intentFilter = IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) - context.registerReceiver(wifiScanReceiver, intentFilter) + DisposableWifiScanCallback( + context = context, + wifiManager = wifiManager, + onReceive = ::sendScanResults, + ) - onDispose { - context.unregisterReceiver(wifiScanReceiver) - } - } - } - - LaunchedEffect(isWifiEnabled) { + LaunchedEffect(state.isWifiEnabled, state.isLocationEnabled) { sendScanResults() - if (isWifiEnabled) { + // Just location can work + if (state.isLocationEnabled) { wifiManager.startScan() } } - val launchAddWifi = rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ) { activityResult -> - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return@rememberLauncherForActivityResult + AddWifiSuggestion( + wifiSuggestions = state.wifiSuggestions, + wifiManager = wifiManager, + onDone = { viewModel.postEvent(Event.ClearWifiEvents) }, + ) - val result = activityResult.data - ?.getIntegerArrayListExtra(Settings.EXTRA_WIFI_NETWORK_RESULT_LIST) - .orEmpty() - - Log.d( - "WIFI", - result.joinToString { - when (it) { - Settings.ADD_WIFI_RESULT_SUCCESS -> "ADD_WIFI_RESULT_SUCCESS" - Settings.ADD_WIFI_RESULT_ADD_OR_UPDATE_FAILED -> "ADD_WIFI_RESULT_ADD_OR_UPDATE_FAILED" - Settings.ADD_WIFI_RESULT_ALREADY_EXISTS -> "ADD_WIFI_RESULT_ALREADY_EXISTS" - else -> "OTHER" - } - }, - ) + ConnectToWifi( + wifiToConnect = state.wifiToConnect, + context = context, + onDone = { viewModel.postEvent(Event.ClearWifiEvents) }, + ) - if (result.any { it == Settings.ADD_WIFI_RESULT_SUCCESS || it == Settings.ADD_WIFI_RESULT_ALREADY_EXISTS }) { - wifiManager.addNetworkSuggestions(state.wifiSuggestions.orEmpty()) - } - viewModel.postEvent(Event.ClearWifiEvents) - } - - LaunchedEffect(state.wifiSuggestions) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@LaunchedEffect + WifiScreen( + state = state, + startActivity = { context.startActivity(it) }, + postEvent = viewModel::postEvent, + ) +} - val suggestions = state.wifiSuggestions - if (suggestions.isNullOrEmpty()) return@LaunchedEffect +@Composable +private fun WifiScreen( + state: State, + startActivity: (Intent) -> Unit, + postEvent: (Event) -> Unit, +) { + var clickedWifiItem by remember { mutableStateOf(null) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val bundle = Bundle().apply { - putParcelableArrayList(Settings.EXTRA_WIFI_NETWORK_LIST, suggestions) + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize(), + ) { + if (!state.isWifiEnabled) { + item { + Text(text = "Wifi is not enabled", modifier = Modifier.padding(8.dp)) + Button( + onClick = { startActivity(Intent(Settings.ACTION_WIFI_SETTINGS)) }, + modifier = Modifier.padding(8.dp), + ) { + Text(text = "Enable wifi") + } } - launchAddWifi.launch(Intent(Settings.ACTION_WIFI_ADD_NETWORKS).putExtras(bundle)) - } else { - // Requires only API Q (29) - wifiManager.addNetworkSuggestions(suggestions) - viewModel.postEvent(Event.ClearWifiEvents) } - } - LaunchedEffect(state.wifiToConnect) { - if (state.wifiToConnect == null) return@LaunchedEffect - - connectivityManager.requestNetwork( - state.wifiToConnect!!, - object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - Log.d("WIFI", "onAvailable") - } - - override fun onUnavailable() { - Log.d("WIFI", "onUnavailable") - } - }, - ) - viewModel.postEvent(Event.ClearWifiEvents) - } - - var clickedWifiItem by remember { mutableStateOf(null) } - LazyColumn { - if (!isWifiEnabled) { + if (!state.isLocationEnabled) { item { - Text(text = "Wifi is not enabled") + Text(text = "Location is not enabled", modifier = Modifier.padding(8.dp)) + Button( + onClick = { startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) }, + modifier = Modifier.padding(8.dp), + ) { + Text(text = "Enable location") + } } } + items(items = state.wifiNetworks) { WifiNetworkItem(state = it, onClick = { clickedWifiItem = it }) } @@ -176,42 +155,48 @@ internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { clickedWifiItem?.let { WifiItemMenu( onDismiss = { clickedWifiItem = null }, - onSave = { viewModel.postEvent(Event.SaveWifi(it)) }, - onConnect = { viewModel.postEvent(Event.ConnectWifi(it)) }, + onSave = { postEvent(Event.SaveWifi(it)) }, + onConnect = { postEvent(Event.ConnectWifi(it)) }, ) } } @Composable -private fun WifiNetworkItem(state: NetworkState, onClick: () -> Unit) { - Card(onClick = onClick, modifier = Modifier.padding(2.dp)) { - Row( - modifier = Modifier.padding(all = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = state.ssid, - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - if (state.channel != -1) { - Text(text = "Channel ${state.channel}", fontSize = 14.sp) - } +private fun WifiNetworkItem( + state: NetworkState, + onClick: () -> Unit +) = Card( + onClick = onClick, + modifier = Modifier + .padding(2.dp) + .fillMaxWidth(), +) { + Row( + modifier = Modifier.padding(all = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = state.ssid, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (state.channel != -1) { + Text(text = "Channel ${state.channel}", fontSize = 14.sp) } + } - if (state.bandGhz.isNotEmpty() && state.channelWidthMhz != -1) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = "${state.bandGhz} GHz") - Text(text = "${state.channelWidthMhz} MHz", fontSize = 14.sp) - } + if (state.bandGhz.isNotEmpty() && state.channelWidthMhz != -1) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = "${state.bandGhz} GHz") + Text(text = "${state.channelWidthMhz} MHz", fontSize = 14.sp) } - - Text(text = "${state.level}") } + + Text(text = "${state.level}") } } @@ -244,3 +229,97 @@ private fun WifiItemMenu( } } } + +@Composable +private fun DisposableWifiScanCallback( + context: Context, + wifiManager: WifiManager, + onReceive: () -> Unit, +) = DisposableEffect(context, wifiManager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val callback = object : ScanResultsCallback() { + override fun onScanResultsAvailable() { + onReceive() + } + } + wifiManager.registerScanResultsCallback( + ContextCompat.getMainExecutor(context), + callback, + ) + + onDispose { + wifiManager.unregisterScanResultsCallback(callback) + } + } else { + val wifiScanReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + onReceive() + } + } + val intentFilter = IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) + context.registerReceiver(wifiScanReceiver, intentFilter) + + onDispose { + context.unregisterReceiver(wifiScanReceiver) + } + } +} + +@Composable +private fun AddWifiSuggestion( + wifiSuggestions: ArrayList?, + wifiManager: WifiManager, + onDone: () -> Unit, +) { + val launchAddWifi = rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ) { activityResult -> + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return@rememberLauncherForActivityResult + + val result = activityResult.data + ?.getIntegerArrayListExtra(Settings.EXTRA_WIFI_NETWORK_RESULT_LIST) + .orEmpty() + + if (result.any { + it == Settings.ADD_WIFI_RESULT_SUCCESS + || it == Settings.ADD_WIFI_RESULT_ALREADY_EXISTS + }) { + wifiManager.addNetworkSuggestions(wifiSuggestions.orEmpty()) + } + onDone() + } + + LaunchedEffect(wifiSuggestions) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@LaunchedEffect + + if (wifiSuggestions.isNullOrEmpty()) return@LaunchedEffect + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val bundle = Bundle() + bundle.putParcelableArrayList(Settings.EXTRA_WIFI_NETWORK_LIST, wifiSuggestions) + launchAddWifi.launch(Intent(Settings.ACTION_WIFI_ADD_NETWORKS).putExtras(bundle)) + } else { + // Requires only API Q (29) + wifiManager.addNetworkSuggestions(wifiSuggestions) + onDone() + } + } +} + +@Composable +private fun ConnectToWifi( + context: Context, + wifiToConnect: NetworkRequest?, + onDone: () -> Unit, +) { + val connectivityManager = remember { context.getSystemService(ConnectivityManager::class.java) } + LaunchedEffect(wifiToConnect) { + if (wifiToConnect == null) return@LaunchedEffect + + connectivityManager.requestNetwork( + wifiToConnect, + ConnectivityManager.NetworkCallback(), + ) + onDone() + } +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt index 665565b..7c36c4a 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt @@ -36,6 +36,8 @@ internal class WifiVM @Inject constructor() : BaseVM() { } Event.ClearWifiEvents -> setState { copy(wifiToConnect = null, wifiSuggestions = null) } + is Event.UpdateLocationEnabled -> setState { copy(isLocationEnabled = event.enabled) } + is Event.UpdateWifiEnabled -> setState { copy(isWifiEnabled = event.enabled) } } } From a47474656fbe0b998667cb06945a463c22c3a1f2 Mon Sep 17 00:00:00 2001 From: retanar Date: Tue, 24 Dec 2024 14:04:43 +0200 Subject: [PATCH 7/8] Location permission request --- .../homeImpl/wifi/WifiContract.kt | 1 + .../featuremodule/homeImpl/wifi/WifiScreen.kt | 88 ++++++++++++++++++- .../com/featuremodule/homeImpl/wifi/WifiVM.kt | 9 +- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt index 21e5170..aaa483a 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiContract.kt @@ -30,4 +30,5 @@ internal sealed interface Event : UiEvent { data object ClearWifiEvents : Event data class UpdateLocationEnabled(val enabled: Boolean) : Event data class UpdateWifiEnabled(val enabled: Boolean) : Event + data object PopBack : Event } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt index 4185ee8..041e1f7 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt @@ -1,9 +1,11 @@ package com.featuremodule.homeImpl.wifi +import android.Manifest import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.PackageManager import android.location.LocationManager import android.net.ConnectivityManager import android.net.NetworkRequest @@ -13,19 +15,24 @@ import android.net.wifi.WifiNetworkSuggestion import android.os.Build import android.os.Bundle import android.provider.Settings +import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Button import androidx.compose.material3.Card +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -38,11 +45,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import androidx.core.content.ContextCompat import androidx.core.location.LocationManagerCompat import androidx.hilt.navigation.compose.hiltViewModel @@ -56,6 +68,7 @@ internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { val wifiManager = remember { context.getSystemService(WifiManager::class.java) } val state by viewModel.state.collectAsStateWithLifecycle() + var hasLocationPermission by remember { mutableStateOf(context.hasLocationPermission()) } val lifecycle = LocalLifecycleOwner.current.lifecycle val locationManager = remember { context.getSystemService(LocationManager::class.java) } @@ -84,10 +97,10 @@ internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { onReceive = ::sendScanResults, ) - LaunchedEffect(state.isWifiEnabled, state.isLocationEnabled) { + LaunchedEffect(state.isWifiEnabled, state.isLocationEnabled, hasLocationPermission) { sendScanResults() // Just location can work - if (state.isLocationEnabled) { + if (hasLocationPermission && state.isLocationEnabled) { wifiManager.startScan() } } @@ -109,6 +122,72 @@ internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { startActivity = { context.startActivity(it) }, postEvent = viewModel::postEvent, ) + + if (!hasLocationPermission) { + LocationPermissionDialog( + onSuccess = { hasLocationPermission = true }, + onFailure = { + viewModel.postEvent(Event.PopBack) + Toast.makeText( + context, + "Precise location permission was not granted", + Toast.LENGTH_LONG, + ).show() + }, + ) + } +} + +@Composable +private fun LocationPermissionDialog( + onSuccess: () -> Unit, + onFailure: () -> Unit, +) { + val launchRequest = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { isGranted -> + if (isGranted) { + onSuccess() + } else { + onFailure() + } + } + + Dialog( + onDismissRequest = onFailure, + properties = DialogProperties(dismissOnClickOutside = false), + ) { + Card { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val title = buildAnnotatedString { + withStyle(SpanStyle(fontWeight = FontWeight.SemiBold)) { + append("Precise location ") + } + append("permission is required to access wifi scanning") + } + Text(text = title, fontSize = 18.sp, textAlign = TextAlign.Center) + Spacer(modifier = Modifier.height(16.dp)) + + Row { + OutlinedButton(onClick = onFailure, modifier = Modifier.weight(1f)) { + Text(text = "Leave") + } + Spacer(modifier = Modifier.width(16.dp)) + Button( + onClick = { + launchRequest.launch(Manifest.permission.ACCESS_FINE_LOCATION) + }, + modifier = Modifier.weight(1f), + ) { + Text(text = "Grant") + } + } + } + } + } } @Composable @@ -323,3 +402,8 @@ private fun ConnectToWifi( onDone() } } + +private fun Context.hasLocationPermission() = ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION, +) == PackageManager.PERMISSION_GRANTED diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt index 7c36c4a..41b8fb1 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt @@ -8,13 +8,17 @@ import android.net.wifi.WifiNetworkSpecifier import android.net.wifi.WifiNetworkSuggestion import android.os.Build import androidx.annotation.RequiresApi +import com.featuremodule.core.navigation.NavCommand +import com.featuremodule.core.navigation.NavManager import com.featuremodule.core.ui.BaseVM import com.featuremodule.core.util.WifiUtils import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -internal class WifiVM @Inject constructor() : BaseVM() { +internal class WifiVM @Inject constructor( + private val navManager: NavManager, +) : BaseVM() { override fun initialState() = State() override fun handleEvent(event: Event) { @@ -38,6 +42,9 @@ internal class WifiVM @Inject constructor() : BaseVM() { Event.ClearWifiEvents -> setState { copy(wifiToConnect = null, wifiSuggestions = null) } is Event.UpdateLocationEnabled -> setState { copy(isLocationEnabled = event.enabled) } is Event.UpdateWifiEnabled -> setState { copy(isWifiEnabled = event.enabled) } + Event.PopBack -> launch { + navManager.navigate(NavCommand.PopBack) + } } } From e771a5b02389c86337ccb3440f4ed30edcc08612 Mon Sep 17 00:00:00 2001 From: retanar Date: Fri, 27 Dec 2024 13:35:35 +0200 Subject: [PATCH 8/8] Linters reformat --- .../featuremodule/homeImpl/wifi/WifiScreen.kt | 79 ++++++++++--------- .../com/featuremodule/homeImpl/wifi/WifiVM.kt | 4 +- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt index 041e1f7..19d2bc2 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiScreen.kt @@ -15,6 +15,7 @@ import android.net.wifi.WifiNetworkSuggestion import android.os.Build import android.os.Bundle import android.provider.Settings +import android.util.Log import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -40,6 +41,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -87,7 +89,7 @@ internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { try { viewModel.postEvent(Event.WifiResultsScanned(wifiManager.scanResults)) } catch (e: SecurityException) { - e.printStackTrace() + Log.e("WifiScreen", "Failed to get scan results", e) } } @@ -139,10 +141,7 @@ internal fun WifiScreen(viewModel: WifiVM = hiltViewModel()) { } @Composable -private fun LocationPermissionDialog( - onSuccess: () -> Unit, - onFailure: () -> Unit, -) { +private fun LocationPermissionDialog(onSuccess: () -> Unit, onFailure: () -> Unit) { val launchRequest = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission(), ) { isGranted -> @@ -241,10 +240,7 @@ private fun WifiScreen( } @Composable -private fun WifiNetworkItem( - state: NetworkState, - onClick: () -> Unit -) = Card( +private fun WifiNetworkItem(state: NetworkState, onClick: () -> Unit) = Card( onClick = onClick, modifier = Modifier .padding(2.dp) @@ -314,42 +310,48 @@ private fun DisposableWifiScanCallback( context: Context, wifiManager: WifiManager, onReceive: () -> Unit, -) = DisposableEffect(context, wifiManager) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val callback = object : ScanResultsCallback() { - override fun onScanResultsAvailable() { - onReceive() +) { + val onReceiveUpdated by rememberUpdatedState(onReceive) + DisposableEffect(context, wifiManager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val callback = object : ScanResultsCallback() { + override fun onScanResultsAvailable() { + onReceiveUpdated() + } } - } - wifiManager.registerScanResultsCallback( - ContextCompat.getMainExecutor(context), - callback, - ) + wifiManager.registerScanResultsCallback( + ContextCompat.getMainExecutor(context), + callback, + ) - onDispose { - wifiManager.unregisterScanResultsCallback(callback) - } - } else { - val wifiScanReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - onReceive() + onDispose { + wifiManager.unregisterScanResultsCallback(callback) } - } - val intentFilter = IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) - context.registerReceiver(wifiScanReceiver, intentFilter) + } else { + val wifiScanReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + onReceiveUpdated() + } + } + val intentFilter = IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) + context.registerReceiver(wifiScanReceiver, intentFilter) - onDispose { - context.unregisterReceiver(wifiScanReceiver) + onDispose { + context.unregisterReceiver(wifiScanReceiver) + } } } } +// Because ArrayList is not mutated, and is used as immutable +@Suppress("ktlint:compose:mutable-params-check") @Composable private fun AddWifiSuggestion( wifiSuggestions: ArrayList?, wifiManager: WifiManager, onDone: () -> Unit, ) { + val onDoneUpdated by rememberUpdatedState(onDone) val launchAddWifi = rememberLauncherForActivityResult( ActivityResultContracts.StartActivityForResult(), ) { activityResult -> @@ -360,17 +362,17 @@ private fun AddWifiSuggestion( .orEmpty() if (result.any { - it == Settings.ADD_WIFI_RESULT_SUCCESS - || it == Settings.ADD_WIFI_RESULT_ALREADY_EXISTS - }) { + it == Settings.ADD_WIFI_RESULT_SUCCESS || + it == Settings.ADD_WIFI_RESULT_ALREADY_EXISTS + } + ) { wifiManager.addNetworkSuggestions(wifiSuggestions.orEmpty()) } - onDone() + onDoneUpdated() } LaunchedEffect(wifiSuggestions) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@LaunchedEffect - if (wifiSuggestions.isNullOrEmpty()) return@LaunchedEffect if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @@ -380,7 +382,7 @@ private fun AddWifiSuggestion( } else { // Requires only API Q (29) wifiManager.addNetworkSuggestions(wifiSuggestions) - onDone() + onDoneUpdated() } } } @@ -391,6 +393,7 @@ private fun ConnectToWifi( wifiToConnect: NetworkRequest?, onDone: () -> Unit, ) { + val onDoneUpdated by rememberUpdatedState(onDone) val connectivityManager = remember { context.getSystemService(ConnectivityManager::class.java) } LaunchedEffect(wifiToConnect) { if (wifiToConnect == null) return@LaunchedEffect @@ -399,7 +402,7 @@ private fun ConnectToWifi( wifiToConnect, ConnectivityManager.NetworkCallback(), ) - onDone() + onDoneUpdated() } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt index 41b8fb1..bf6d84b 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/wifi/WifiVM.kt @@ -56,9 +56,7 @@ internal class WifiVM @Inject constructor( channelWidthMhz = when (channelWidth) { ScanResult.CHANNEL_WIDTH_20MHZ -> 20 ScanResult.CHANNEL_WIDTH_40MHZ -> 40 - ScanResult.CHANNEL_WIDTH_80MHZ, - ScanResult.CHANNEL_WIDTH_80MHZ_PLUS_MHZ -> 80 - + ScanResult.CHANNEL_WIDTH_80MHZ, ScanResult.CHANNEL_WIDTH_80MHZ_PLUS_MHZ -> 80 ScanResult.CHANNEL_WIDTH_160MHZ -> 160 ScanResult.CHANNEL_WIDTH_320MHZ -> 320 else -> -1