From 321d71249d178fef5c22aa2245ca8877b663f3a8 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 3 May 2024 13:18:53 +0200 Subject: [PATCH 01/78] feature/nfc --- app/build.gradle.kts | 1 + .../no/nordicsemi/android/wifi/provisioner/MainActivity.kt | 4 +++- settings.gradle.kts | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6dff163d..c02bcbbe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -44,6 +44,7 @@ android { dependencies { implementation(project(":feature:ble")) implementation(project(":feature:softap")) + implementation(project(":feature:nfc")) implementation(project(":feature:common")) implementation(project(":feature:ui")) implementation(project(":lib:ble")) diff --git a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt index c840937f..c086b2d3 100644 --- a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt +++ b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt @@ -45,6 +45,7 @@ import no.nordicsemi.android.common.navigation.NavigationView import no.nordicsemi.android.common.theme.NordicActivity import no.nordicsemi.android.common.theme.NordicTheme import no.nordicsemi.android.wifi.provisioner.ble.view.BleProvisioningDestinations +import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.NfcProvisionerDestinations import no.nordicsemi.android.wifi.provisioner.softap.view.SoftApProvisionerDestinations @AndroidEntryPoint @@ -60,7 +61,8 @@ class MainActivity : NordicActivity() { Surface(modifier = Modifier.fillMaxSize()) { NavigationView( destinations = (HomeDestination + - BleProvisioningDestinations).run { + BleProvisioningDestinations + + NfcProvisionerDestinations).run { // Soft AP is available on Android 10 and newer. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { this + SoftApProvisionerDestinations diff --git a/settings.gradle.kts b/settings.gradle.kts index fcd7a520..6acd5314 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,10 +57,12 @@ rootProject.name = "Android-nRF-Wifi-Provisioner" include(":app") include(":feature:ble") include(":feature:softap") +include(":feature:nfc") include(":feature:ui") include(":feature:common") include(":lib:ble") include(":lib:softap") +include(":lib:nfc") include(":lib:domain") //if (file('../Android-Common-Libraries').exists()) { From 0b44c9686a5b13faa012b576e0222f6641f32002 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 3 May 2024 13:20:02 +0200 Subject: [PATCH 02/78] nfc module --- feature/nfc/build.gradle.kts | 20 ++ feature/nfc/src/main/AndroidManifest.xml | 12 + .../nfc/viemodel/NfcProvisioningViewEvent.kt | 16 ++ .../nfc/viemodel/NfcProvisioningViewModel.kt | 70 ++++++ .../feature/nfc/view/NfcDestinations.kt | 30 +++ .../feature/nfc/view/NfcProvisioningScreen.kt | 208 ++++++++++++++++++ .../nfc/view/NfcRecordOutlinedCardItem.kt | 88 ++++++++ .../nfc/view/WiFiAccessPointsForNfcScreen.kt | 12 + .../nfc/viewmodel/WifiScannerViewModel.kt | 29 +++ lib/nfc/provisioner/build.gradle.kts | 15 ++ .../provisioner/src/main/AndroidManifest.xml | 19 ++ .../provisioner/nfc/WifiManagerRepository.kt | 63 ++++++ .../wifi/provisioner/nfc/di/WifiModule.kt | 21 ++ 13 files changed, 603 insertions(+) create mode 100644 feature/nfc/build.gradle.kts create mode 100644 feature/nfc/src/main/AndroidManifest.xml create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcDestinations.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcRecordOutlinedCardItem.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WiFiAccessPointsForNfcScreen.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt create mode 100644 lib/nfc/provisioner/build.gradle.kts create mode 100644 lib/nfc/provisioner/src/main/AndroidManifest.xml create mode 100644 lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt create mode 100644 lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiModule.kt diff --git a/feature/nfc/build.gradle.kts b/feature/nfc/build.gradle.kts new file mode 100644 index 00000000..df1903aa --- /dev/null +++ b/feature/nfc/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(libs.plugins.nordic.feature) + alias(libs.plugins.nordic.hilt) +} + +android { + namespace = "no.nordicsemi.android.wifi.provisioner.feature.nfc" +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.nordic.theme) + implementation(libs.nordic.navigation) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.compose.material.iconsExtended) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(project(":feature:common")) + api(project(":lib:nfc:provisioner")) +} \ No newline at end of file diff --git a/feature/nfc/src/main/AndroidManifest.xml b/feature/nfc/src/main/AndroidManifest.xml new file mode 100644 index 00000000..61984b6c --- /dev/null +++ b/feature/nfc/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt new file mode 100644 index 00000000..332ec04f --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt @@ -0,0 +1,16 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel + +/** + * A sealed class to represent the events that can be triggered from the UI. + */ +internal sealed interface NfcProvisioningViewEvent + +/** + * Event triggered when the wifi ssid scan button is clicked. + */ +internal data object OnScanClickEvent : NfcProvisioningViewEvent + +/** + * Event triggered when the back button is clicked. + */ +internal data object OnBackClickEvent: NfcProvisioningViewEvent diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt new file mode 100644 index 00000000..adbdbee4 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt @@ -0,0 +1,70 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.wifi.provisioner.nfc.NfcManager +import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository +import javax.inject.Inject + +@HiltViewModel +internal class NfcProvisioningViewModel @Inject constructor( + private val navigator: Navigator, + private val nfcManager: NfcManager, + private val wifiManager: WifiManagerRepository, +) : ViewModel() { + + fun onEvent(event: NfcProvisioningViewEvent) { + when (event) { + is OnScanClickEvent -> { + // TODO: Implement provision click event + // Navigate to the Scanning screen. + listSsids() + } + + OnBackClickEvent -> navigator.navigateUp() + } + } + + private fun listSsids() { + val handler = CoroutineExceptionHandler { _, exception -> + exception.printStackTrace() + } + viewModelScope.launch(handler) { + try { + val result = wifiManager.startWifiScan() + wifiManager.wifiNetworks.onEach { scanResults -> + Log.d("AAA", "WifiNetworks: $scanResults") + scanResults.onEach { + Log.d("AAA", "ScanResult: $it") + } + }.launchIn(viewModelScope) + Log.d("AAA", "listSsids: $result") + } catch (e: Exception) { + e.printStackTrace() + Log.e("AAA", "Error: ${e.message}") + } + /*val result = nfcManager.listSsids() + Log.d("AAAA", "Results: ${result.results}") + result.results.forEach { + it.wifiInfo?.takeIf { wifiInfo -> + wifiInfo.ssid == "OnHub" + }?.let { wifiInfo -> + Log.d("AAAA", "Found the device!") + val wifiConfig = WifiConfigDomain( + info = wifiInfo, + passphrase = "newbird379", + ) + Log.d("AAAA", "WifiConfig: $wifiConfig") + return@forEach + } + }*/ + } + } +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcDestinations.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcDestinations.kt new file mode 100644 index 00000000..ed5a0b85 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcDestinations.kt @@ -0,0 +1,30 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.view + +import android.os.Build +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.common.navigation.createDestination +import no.nordicsemi.android.common.navigation.createSimpleDestination +import no.nordicsemi.android.common.navigation.defineDestination +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewModel +import no.nordicsemi.kotlin.wifi.provisioner.feature.common.WifiData + + +val NfcProvisionerDestinationId = createSimpleDestination("nfc-provider-destination") +val WiFiAccessPointsDestinationIdForNfc = createDestination( + name = "wifi-access-points-destination2" +) + +val NfcProvisionerDestinations = listOf( + defineDestination(NfcProvisionerDestinationId) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + NfcProvisioningScreen() + } + }, + defineDestination(WiFiAccessPointsDestinationIdForNfc) { + val viewModel = hiltViewModel() + val viewEntity by viewModel.state.collectAsStateWithLifecycle() + WiFiAccessPointsForNfcScreen(viewEntity, viewModel::onEvent) + } +) \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt new file mode 100644 index 00000000..2a102587 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt @@ -0,0 +1,208 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.view + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Wifi +import androidx.compose.material.icons.filled.WifiFind +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material.icons.outlined.Wifi +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import no.nordicsemi.android.common.theme.view.NordicAppBar +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.NfcProvisioningViewEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.NfcProvisioningViewModel +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.OnBackClickEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.OnScanClickEvent + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun NfcProvisioningScreen() { + val viewModel: NfcProvisioningViewModel = hiltViewModel() + val onEvent: (NfcProvisioningViewEvent) -> Unit = { viewModel.onEvent(it) } + var openAddWifiDialog by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 56.dp) + ) { + NordicAppBar( + text = "Wifi Provisioning over NFC", + showBackButton = true, + onNavigationButtonClick = { onEvent(OnBackClickEvent) } + ) + // Content + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .weight(1f) + .padding(8.dp) + ) { + // Show an option to enter the WiFi credentials manually. + OutlinedCard( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .clickable { + openAddWifiDialog = true + } + + ) { + // TODO: Once this is merged with NFC app, replace this with the one from NFC app. + NfcRecordOutlinedCardItem( + headline = "Enter WiFi credentials manually", + description = { + Text( + text = "Add WiFi credentials manually.", + style = MaterialTheme.typography.bodyMedium, + ) + }, + icon = Icons.Default.Wifi, + ) { + Spacer(Modifier.weight(1f)) + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .clickable { + openAddWifiDialog = true + } + .padding(8.dp) + ) + } + } + + // Show an option to search for a WiFi network. + OutlinedCard( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .clickable { + // TODO: Show a list of WiFi networks. + + onEvent(OnScanClickEvent) + } + + ) { + NfcRecordOutlinedCardItem( + headline = "Search for WiFi networks", + description = { + Text( + text = "Search for available WiFi networks.", + style = MaterialTheme.typography.bodyMedium, + ) + }, + icon = Icons.Default.WifiFind, + ) { + Spacer(Modifier.weight(1f)) + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .clickable { + // TODO: Show a list of WiFi networks. + onEvent(OnScanClickEvent) + } + .padding(8.dp) + ) + } + } + if (openAddWifiDialog) { + // Open a dialog to enter the WiFi credentials manually. + AlertDialog( + onDismissRequest = { }, + icon = { + Icon(imageVector = Icons.Outlined.Wifi, contentDescription = null) + }, + title = { + Text( + text = "Setup WiFi", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + }, + text = { + Column { + var ssid by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var showPassword by remember { mutableStateOf(false) } + Text( + text = "Setup WiFi", + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.size(size = 16.dp)) + OutlinedTextField(value = ssid, onValueChange = { ssid = it }) + Spacer(modifier = Modifier.size(size = 8.dp)) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + visualTransformation = if (showPassword) + VisualTransformation.None + else + PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { showPassword = !showPassword }) { + Icon( + imageVector = if (!showPassword) + Icons.Outlined.Visibility + else Icons.Outlined.VisibilityOff, + contentDescription = null + ) + } + } + ) + } + }, + dismissButton = { + TextButton( + onClick = { + openAddWifiDialog = false + } + ) { Text(text = "Cancel") } + }, + confirmButton = { + TextButton( + onClick = { + openAddWifiDialog = false + // TODO: Connect to the WiFi network. + } + ) { Text(text = "Confirm") } + } + ) + } + } + } +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcRecordOutlinedCardItem.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcRecordOutlinedCardItem.kt new file mode 100644 index 00000000..9dd600ab --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcRecordOutlinedCardItem.kt @@ -0,0 +1,88 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.TipsAndUpdates +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.common.theme.NordicTheme +import no.nordicsemi.android.common.theme.nordicBlue + + +/** Compose view for the NFC record item in the outlined card. + * @param headline The headline of the record. + * @param description The description of the record. + * @param icon The icon of the record. + * @param content The content of the record. + */ +@Composable +fun NfcRecordOutlinedCardItem( + headline: String, + description: @Composable (RowScope.(TextStyle) -> Unit), + icon: ImageVector, + content: @Composable (RowScope.() -> Unit), +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.nordicBlue, + modifier = Modifier.size(28.dp) + ) + + Column( + modifier = Modifier.padding(8.dp) + ) { + Text( + text = headline, + style = MaterialTheme.typography.titleMedium, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.alpha(0.7f) + ) { + description(MaterialTheme.typography.bodySmall) + } + } + content() + } + +} + +@Preview +@Composable +private fun NfcRecordOutlinedCardItemPreview() { + NordicTheme { + NfcRecordOutlinedCardItem( + headline = "URI record", + description = { + Text( + text = "https://www.nordicsemi.no", + style = it, + ) + }, + icon = Icons.Default.TipsAndUpdates, + ) { + } + } +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WiFiAccessPointsForNfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WiFiAccessPointsForNfcScreen.kt new file mode 100644 index 00000000..1b5fb8f7 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WiFiAccessPointsForNfcScreen.kt @@ -0,0 +1,12 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.view + +import androidx.compose.runtime.Composable +import no.nordicsemi.kotlin.wifi.provisioner.feature.common.WifiScannerViewEntity +import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.WifiScannerViewEvent + +@Composable +internal fun WiFiAccessPointsForNfcScreen( + viewEntity: WifiScannerViewEntity, + onEvent: (WifiScannerViewEvent) -> Unit +) { +} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt new file mode 100644 index 00000000..4d367b0d --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -0,0 +1,29 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel + +import dagger.hilt.android.lifecycle.HiltViewModel +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.WiFiAccessPointsDestinationIdForNfc +import no.nordicsemi.kotlin.wifi.provisioner.feature.common.WifiAggregator +import no.nordicsemi.kotlin.wifi.provisioner.feature.common.WifiData +import no.nordicsemi.kotlin.wifi.provisioner.feature.common.viewmodel.GenericWifiScannerViewModel +import javax.inject.Inject + +@HiltViewModel +internal class WifiScannerViewModel @Inject constructor( + navigationManager: Navigator, + wifiAggregator: WifiAggregator, +) : GenericWifiScannerViewModel( + navigationManager = navigationManager, + wifiAggregator = wifiAggregator +) { + override fun navigateUp() { + navigationManager.navigateUp() + } + + override fun navigateUp(wifiData: WifiData) { + navigationManager.navigateUpWithResult( + from = WiFiAccessPointsDestinationIdForNfc, + result = wifiData + ) + } +} \ No newline at end of file diff --git a/lib/nfc/provisioner/build.gradle.kts b/lib/nfc/provisioner/build.gradle.kts new file mode 100644 index 00000000..eccf533c --- /dev/null +++ b/lib/nfc/provisioner/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(libs.plugins.nordic.library) + alias(libs.plugins.nordic.kotlin) + alias(libs.plugins.nordic.hilt) +} + +android { + namespace = "no.nordicsemi.android.wifi.provisioner.nfc" +} + +dependencies { + implementation(libs.nordic.ble.ktx) + implementation(libs.nordic.ble.common) + api(project(":lib:softap:provisioner")) +} \ No newline at end of file diff --git a/lib/nfc/provisioner/src/main/AndroidManifest.xml b/lib/nfc/provisioner/src/main/AndroidManifest.xml new file mode 100644 index 00000000..26d0816b --- /dev/null +++ b/lib/nfc/provisioner/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt new file mode 100644 index 00000000..143c5730 --- /dev/null +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt @@ -0,0 +1,63 @@ +package no.nordicsemi.android.wifi.provisioner.nfc + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.wifi.ScanResult +import android.net.wifi.WifiManager +import android.util.Log +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import no.nordicsemi.android.wifi.provisioner.softap.SoftApManager +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NfcManager @Inject constructor( + private val softApManager: SoftApManager, +) { + + /** + * Lists the SSIDs scanned by the nRF7002 device + */ + suspend fun listSsids() = softApManager.listSsids() +} + +@Singleton +@SuppressLint("MissingPermission") +class WifiManagerRepository @Inject constructor( + private val wifiManager: WifiManager, + @ApplicationContext private val context: Context, +) { + private val _wifiNetworks = MutableStateFlow>(emptyList()) + val wifiNetworks = _wifiNetworks.asStateFlow() + + init { + if (!wifiManager.isWifiEnabled) { + wifiManager.isWifiEnabled = true + } + val wifiInfo = wifiManager.connectionInfo?.let { + Log.d("AAA", "Connected to: ${it.ssid}") + } + // Register BroadcastReceiver to receive scan results + val wifiScanReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) { + _wifiNetworks.value = wifiManager.scanResults + Log.d("AAA", "onReceive: ${wifiManager.scanResults}") + } + } + } + context.registerReceiver( + wifiScanReceiver, + IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) + ) + } + + fun startWifiScan() { + wifiManager.startScan() + } +} \ No newline at end of file diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiModule.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiModule.kt new file mode 100644 index 00000000..1321701a --- /dev/null +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiModule.kt @@ -0,0 +1,21 @@ +package no.nordicsemi.android.wifi.provisioner.nfc.di + +import android.content.Context +import android.net.wifi.WifiManager +import android.nfc.NfcAdapter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object WifiModule { + @Provides + @Singleton + fun provideWifiManager(@ApplicationContext context: Context): WifiManager { + return context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + } +} \ No newline at end of file From 4dc9777322eb808186c7ddf6cd7c20839252b936 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 10 May 2024 16:17:30 +0200 Subject: [PATCH 03/78] wifi permission --- feature/nfc/src/main/AndroidManifest.xml | 17 +-- .../nfc/permission/RequireLocationForWifi.kt | 36 ++++++ .../feature/nfc/permission/RequireWifi.kt | 50 +++++++++ .../location/LocationStateManager.kt | 80 +++++++++++++ .../nfc/permission/utils/LocalDataProvider.kt | 63 +++++++++++ .../nfc/permission/utils/PermissionUtils.kt | 90 +++++++++++++++ .../permission/utils/WifiPermissionState.kt | 29 +++++ .../view/LocationPermissionRequiredView.kt | 106 ++++++++++++++++++ .../nfc/permission/view/WifiDisabledView.kt | 49 ++++++++ .../permission/view/WifiNotAvailableView.kt | 34 ++++++ .../view/WifiPermissionRequiredView.kt | 92 +++++++++++++++ .../viewmodel/PermissionViewModel.kt | 55 +++++++++ feature/nfc/src/main/res/values/strings.xml | 17 +++ lib/nfc/provisioner/build.gradle.kts | 1 - .../provisioner/src/main/AndroidManifest.xml | 19 +--- 15 files changed, 711 insertions(+), 27 deletions(-) create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/RequireLocationForWifi.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/RequireWifi.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/location/LocationStateManager.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/LocalDataProvider.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/PermissionUtils.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/WifiPermissionState.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/LocationPermissionRequiredView.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiDisabledView.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiNotAvailableView.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiPermissionRequiredView.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/viewmodel/PermissionViewModel.kt create mode 100644 feature/nfc/src/main/res/values/strings.xml diff --git a/feature/nfc/src/main/AndroidManifest.xml b/feature/nfc/src/main/AndroidManifest.xml index 61984b6c..06a6b1da 100644 --- a/feature/nfc/src/main/AndroidManifest.xml +++ b/feature/nfc/src/main/AndroidManifest.xml @@ -1,12 +1,13 @@ - - + + - - - + + + + \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/RequireLocationForWifi.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/RequireLocationForWifi.kt new file mode 100644 index 00000000..04eb6540 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/RequireLocationForWifi.kt @@ -0,0 +1,36 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.permission + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.WifiPermissionNotAvailableReason +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.WifiPermissionState +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.view.LocationPermissionRequiredView +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.viewmodel.PermissionViewModel + +@Composable +fun RequireLocationForWifi( + onChanged: (Boolean) -> Unit = {}, + contentWithoutLocation: @Composable () -> Unit = { LocationPermissionRequiredView() }, + content: @Composable (isLocationRequiredAndDisabled: Boolean) -> Unit, +) { + val viewModel = hiltViewModel() + val state by viewModel.locationPermission.collectAsStateWithLifecycle() + + LaunchedEffect(state) { + onChanged( + state is WifiPermissionState.Available || + (state as WifiPermissionState.NotAvailable).reason == WifiPermissionNotAvailableReason.DISABLED + ) + } + + when (val s = state) { + WifiPermissionState.Available -> content(false) + is WifiPermissionState.NotAvailable -> when (s.reason) { + WifiPermissionNotAvailableReason.DISABLED -> content(true) + else -> contentWithoutLocation() + } + } +} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/RequireWifi.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/RequireWifi.kt new file mode 100644 index 00000000..b0eaa2cb --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/RequireWifi.kt @@ -0,0 +1,50 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.permission + +import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.WifiPermissionNotAvailableReason +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.WifiPermissionState +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.view.WifiDisabledView +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.view.WifiNotAvailableView +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.view.WifiPermissionRequiredView +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.viewmodel.PermissionViewModel + +@Composable +fun RequireWifi( + onChanged: (Boolean) -> Unit = {}, + contentWithoutWifi: @Composable (WifiPermissionNotAvailableReason) -> Unit = { + NoWifiView(reason = it) + }, + content: @Composable () -> Unit, +) { + val viewModel = hiltViewModel() + val state by viewModel.wifiState.collectAsStateWithLifecycle() + + LaunchedEffect(state) { + onChanged(state is WifiPermissionState.Available) + } + + when (val s = state) { + WifiPermissionState.Available -> content() + is WifiPermissionState.NotAvailable -> contentWithoutWifi(s.reason) + } +} + +@Composable +private fun NoWifiView( + reason: WifiPermissionNotAvailableReason, +) { + when (reason) { + WifiPermissionNotAvailableReason.NOT_AVAILABLE -> WifiNotAvailableView() + WifiPermissionNotAvailableReason.PERMISSION_REQUIRED -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + WifiPermissionRequiredView() + } + + WifiPermissionNotAvailableReason.DISABLED -> WifiDisabledView() + } +} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/location/LocationStateManager.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/location/LocationStateManager.kt new file mode 100644 index 00000000..951d53fd --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/location/LocationStateManager.kt @@ -0,0 +1,80 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.location + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.location.LocationManager +import androidx.core.content.ContextCompat +import androidx.core.location.LocationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.LocalDataProvider +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.PermissionUtils +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.WifiPermissionNotAvailableReason +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.WifiPermissionState +import javax.inject.Inject +import javax.inject.Singleton + +private const val REFRESH_PERMISSIONS = + "no.nordicsemi.android.common.permission.REFRESH_LOCATION_PERMISSIONS" + +@Singleton +class LocationStateManager @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val dataProvider = LocalDataProvider(context) + private val utils = PermissionUtils(context, dataProvider) + + @SuppressLint("WrongConstant") + fun locationState() = callbackFlow { + trySend(getLocationState()) + + val locationStateChangeHandler = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + trySend(getLocationState()) + } + } + val filter = IntentFilter().apply { + addAction(LocationManager.MODE_CHANGED_ACTION) + addAction(REFRESH_PERMISSIONS) + } + ContextCompat.registerReceiver( + context, + locationStateChangeHandler, + filter, + ContextCompat.RECEIVER_EXPORTED + ) + awaitClose { + context.unregisterReceiver(locationStateChangeHandler) + } + } + + fun refreshPermission() { + val intent = Intent(REFRESH_PERMISSIONS) + context.sendBroadcast(intent) + } + + fun markLocationPermissionRequested() { + dataProvider.locationPermissionRequested = true + } + + fun isLocationPermissionDeniedForever(context: Context): Boolean { + return utils.isLocationPermissionDeniedForever(context) + } + + private fun getLocationState(): WifiPermissionState { + val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return when { + !utils.isLocationPermissionGranted -> + WifiPermissionState.NotAvailable(WifiPermissionNotAvailableReason.PERMISSION_REQUIRED) + + dataProvider.isLocationPermissionRequired && !LocationManagerCompat.isLocationEnabled(lm) -> + WifiPermissionState.NotAvailable(WifiPermissionNotAvailableReason.DISABLED) + + else -> WifiPermissionState.Available + } + } +} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/LocalDataProvider.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/LocalDataProvider.kt new file mode 100644 index 00000000..81954914 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/LocalDataProvider.kt @@ -0,0 +1,63 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils + +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast +import androidx.core.app.ActivityCompat + +private const val SHARED_PREFS_NAME = "SHARED_PREFS_NAME" + +private const val PREFS_PERMISSION_REQUESTED = "permission_requested" +private const val PREFS_WIFI_PERMISSION_REQUESTED = "wifi_permission_requested" + +internal class LocalDataProvider( + private val context: Context +) { + private val sharedPrefs: SharedPreferences + get() = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + + /** + * The first time an app requests a permission there is no 'Don't ask again' checkbox and + * [ActivityCompat.shouldShowRequestPermissionRationale] returns false. + * This situation is similar to a permission being denied forever, so to distinguish both cases + * a flag needs to be saved. + */ + var locationPermissionRequested: Boolean + get() = sharedPrefs.getBoolean(PREFS_PERMISSION_REQUESTED, false) + set(value) { + sharedPrefs.edit().putBoolean(PREFS_PERMISSION_REQUESTED, value).apply() + } + + /** + * The first time an app requests a permission there is no 'Don't ask again' checkbox and + * [ActivityCompat.shouldShowRequestPermissionRationale] returns false. + * This situation is similar to a permission being denied forever, so to distinguish both cases + * a flag needs to be saved. + */ + var wifiPermissionRequested: Boolean + get() = sharedPrefs.getBoolean(PREFS_WIFI_PERMISSION_REQUESTED, false) + set(value) { + sharedPrefs.edit().putBoolean(PREFS_WIFI_PERMISSION_REQUESTED, value).apply() + } + + val isLocationPermissionRequired: Boolean + /** + * Location enabled is required on phones running Android 6 - 11 + * (for example on Nexus and Pixel devices). Initially, Samsung phones didn't require it, + * but that has been fixed for those phones in Android 9. + * Several Wi-Fi APIs require the ACCESS_FINE_LOCATION permission, + * even when your app targets Android 13 or higher. + * + * @return False if it is known that location is not required, true otherwise. + */ + get() = isMarshmallowOrAbove + + val isMarshmallowOrAbove: Boolean + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.M) + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + + val isTiramisuOrAbove: Boolean + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/PermissionUtils.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/PermissionUtils.kt new file mode 100644 index 00000000..be99e69f --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/PermissionUtils.kt @@ -0,0 +1,90 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.content.pm.PackageManager +import android.location.LocationManager +import android.net.wifi.WifiManager +import androidx.core.content.ContextCompat +import androidx.core.location.LocationManagerCompat + +internal class PermissionUtils( + private val context: Context, + private val dataProvider: LocalDataProvider, +) { + val isWifiEnabled: Boolean + get() = (context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager) + .isWifiEnabled + + val isLocationEnabled: Boolean + get() = if (dataProvider.isMarshmallowOrAbove) { + val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + LocationManagerCompat.isLocationEnabled(lm) + } else true + + val isWifiAvailable: Boolean + get() = context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI) + + private val isLocationPermissionRequired: Boolean + get() = dataProvider.isMarshmallowOrAbove + + private val isWifiPermissionGranted: Boolean + get() = !dataProvider.isTiramisuOrAbove || + ContextCompat.checkSelfPermission( + context, + Manifest.permission.NEARBY_WIFI_DEVICES + ) == PackageManager.PERMISSION_GRANTED + + val isLocationPermissionGranted: Boolean + get() = !isLocationPermissionRequired || + ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + + val areNecessaryWifiPermissionsGranted: Boolean + get() = isWifiPermissionGranted + + fun markWifiPermissionRequested() { + dataProvider.wifiPermissionRequested = true + } + + fun markLocationPermissionRequested() { + dataProvider.locationPermissionRequested = true + } + + fun isWifiPermissionDeniedForever(context: Context): Boolean { + return dataProvider.isTiramisuOrAbove && + !isWifiPermissionGranted && // Wifi permission must be denied + dataProvider.wifiPermissionRequested && // Permission must have been requested before + !context.findActivity() + .shouldShowRequestPermissionRationale(Manifest.permission.NEARBY_WIFI_DEVICES) + } + + fun isLocationPermissionDeniedForever(context: Context): Boolean { + return dataProvider.isMarshmallowOrAbove && + !isLocationPermissionGranted // Location permission must be denied + && dataProvider.locationPermissionRequested // Permission must have been requested before + && !context.findActivity() + .shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) + } + + /** + * Finds the activity from the given context. + * + * https://github.com/google/accompanist/blob/6611ebda55eb2948eca9e1c89c2519e80300855a/permissions/src/main/java/com/google/accompanist/permissions/PermissionsUtil.kt#L99 + * + * @throws IllegalStateException if no activity was found. + * @return the activity. + */ + private fun Context.findActivity(): Activity { + var context = this + while (context is ContextWrapper) { + if (context is Activity) return context + context = context.baseContext + } + throw IllegalStateException("no activity") + } +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/WifiPermissionState.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/WifiPermissionState.kt new file mode 100644 index 00000000..833f9199 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/WifiPermissionState.kt @@ -0,0 +1,29 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils + +/** + * Represents the reason for Wi-Fi permission is not available. + */ +enum class WifiPermissionNotAvailableReason { + PERMISSION_REQUIRED, + NOT_AVAILABLE, + DISABLED, +} + +/** + * Represents the state of Wi-Fi permission. + */ +sealed class WifiPermissionState { + + /** + * Represents the Wi-Fi permission is available. + */ + data object Available : WifiPermissionState() + + /** + * Represents the Wi-Fi permission is not available. + * @param reason The reason for Wi-Fi permission is not available. + */ + data class NotAvailable( + val reason: WifiPermissionNotAvailableReason, + ) : WifiPermissionState() +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/LocationPermissionRequiredView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/LocationPermissionRequiredView.kt new file mode 100644 index 00000000..c3af507d --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/LocationPermissionRequiredView.kt @@ -0,0 +1,106 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.view + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.LocationOff +import androidx.compose.material3.Button +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.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.content.ContextCompat +import androidx.hilt.navigation.compose.hiltViewModel +import no.nordicsemi.android.common.theme.NordicTheme +import no.nordicsemi.android.common.theme.view.WarningView +import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.viewmodel.PermissionViewModel + +@Composable +internal fun LocationPermissionRequiredView() { + val viewModel = hiltViewModel() + val context = LocalContext.current + var permissionDenied by remember { mutableStateOf(viewModel.isLocationPermissionDeniedForever(context)) } + + val launcher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { + viewModel.markLocationPermissionRequested() + permissionDenied = viewModel.isLocationPermissionDeniedForever(context) + viewModel.refreshLocationPermission() + } + + LocationPermissionRequiredView( + permissionDenied = permissionDenied, + onGrantClicked = { + val requiredPermissions = arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION + ) + launcher.launch(requiredPermissions) + }, + onOpenSettingsClicked = { openPermissionSettings(context) }, + ) +} + +@Composable +internal fun LocationPermissionRequiredView( + permissionDenied: Boolean, + onGrantClicked: () -> Unit, + onOpenSettingsClicked: () -> Unit, +) { + WarningView( + imageVector = Icons.Default.LocationOff, + title = stringResource(id = R.string.location_permission_required), + hint = stringResource(id = R.string.location_permission__required_des), + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + if (!permissionDenied) { + Button(onClick = onGrantClicked) { + Text(text = stringResource(id = R.string.grant_permission)) + } + } else { + Button(onClick = onOpenSettingsClicked) { + Text(text = stringResource(id = R.string.settings)) + } + } + } +} + +private fun openPermissionSettings(context: Context) { + ContextCompat.startActivity( + context, + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null) + ), + null + ) +} + +@Preview +@Composable +private fun LocationPermissionRequiredView_Preview() { + NordicTheme { + LocationPermissionRequiredView( + permissionDenied = false, + onGrantClicked = { }, + onOpenSettingsClicked = { }, + ) + } +} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiDisabledView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiDisabledView.kt new file mode 100644 index 00000000..1eac0744 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiDisabledView.kt @@ -0,0 +1,49 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.view + +import android.content.Context +import android.content.Intent +import android.provider.Settings +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.WifiOff +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import no.nordicsemi.android.common.theme.NordicTheme +import no.nordicsemi.android.common.theme.view.WarningView +import no.nordicsemi.android.wifi.provisioner.feature.nfc.R + +@Composable +internal fun WifiDisabledView() { + WarningView( + imageVector = Icons.Default.WifiOff, + title = stringResource(id = R.string.wifi_disabled), + hint = stringResource(id = R.string.wifi_disabled_des), + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + val context = LocalContext.current + Button(onClick = { enableWifi(context) }) { + Text(text = stringResource(id = R.string.enable_wifi)) + } + } +} + +private fun enableWifi(context: Context) { + context.startActivity(Intent(Settings.ACTION_WIFI_SETTINGS)) +} + +@Preview +@Composable +private fun WifiDisabledViewPreview() { + NordicTheme { + WifiDisabledView() + } +} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiNotAvailableView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiNotAvailableView.kt new file mode 100644 index 00000000..0273abdb --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiNotAvailableView.kt @@ -0,0 +1,34 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.view + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.WifiOff +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import no.nordicsemi.android.common.theme.NordicTheme +import no.nordicsemi.android.common.theme.view.WarningView +import no.nordicsemi.android.wifi.provisioner.feature.nfc.R + +@Composable +internal fun WifiNotAvailableView() { + WarningView( + imageVector = Icons.Default.WifiOff, + title = stringResource(id = R.string.wifi_not_available), + hint = stringResource(id = R.string.wifi_not_available_des), + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) +} + +@Preview +@Composable +private fun BluetoothNotAvailableView_Preview() { + NordicTheme { + WifiNotAvailableView() + } +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiPermissionRequiredView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiPermissionRequiredView.kt new file mode 100644 index 00000000..e937ef6a --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiPermissionRequiredView.kt @@ -0,0 +1,92 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.view + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.WifiOff +import androidx.compose.material3.Button +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.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.content.ContextCompat +import androidx.hilt.navigation.compose.hiltViewModel +import no.nordicsemi.android.common.theme.NordicTheme +import no.nordicsemi.android.common.theme.view.WarningView +import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.viewmodel.PermissionViewModel + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +@Composable +internal fun WifiPermissionRequiredView() { + val viewModel: PermissionViewModel = hiltViewModel() + val context = LocalContext.current + var permissionDenied by remember { mutableStateOf(viewModel.isWifiPermissionDeniedForever(context)) } + + WarningView( + imageVector = Icons.Default.WifiOff, + title = stringResource(id = R.string.wifi_permission_required), + hint = stringResource(id = R.string.wifi_permission_required_des), + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + val requiredPermissions = arrayOf( + Manifest.permission.NEARBY_WIFI_DEVICES, + ) + + val launcher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { + viewModel.markWifiPermissionRequested() + permissionDenied = viewModel.isWifiPermissionDeniedForever(context) + viewModel.refreshWifiPermission() + } + + if (!permissionDenied) { + Button(onClick = { launcher.launch(requiredPermissions) }) { + Text(text = stringResource(id = R.string.grant_permission)) + } + } else { + Button(onClick = { openPermissionSettings(context) }) { + Text(text = stringResource(id = R.string.settings)) + } + } + } +} + +private fun openPermissionSettings(context: Context) { + ContextCompat.startActivity( + context, + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null) + ), + null + ) +} + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +@Preview +@Composable +private fun WifiPermissionRequiredViewPreview() { + NordicTheme { + WifiPermissionRequiredView() + } +} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/viewmodel/PermissionViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/viewmodel/PermissionViewModel.kt new file mode 100644 index 00000000..fcdf3b8e --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/viewmodel/PermissionViewModel.kt @@ -0,0 +1,55 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.viewmodel + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.location.LocationStateManager +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.WifiPermissionNotAvailableReason +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.WifiPermissionState +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.wifi.WifiStateManager +import javax.inject.Inject + +@HiltViewModel +class PermissionViewModel @Inject constructor( + private val wifiStateManager: WifiStateManager, + private val locationManager: LocationStateManager, +) : ViewModel() { + val wifiState = wifiStateManager.wifiState() + .stateIn( + viewModelScope, SharingStarted.Lazily, + WifiPermissionState.NotAvailable(WifiPermissionNotAvailableReason.NOT_AVAILABLE) + ) + + val locationPermission = locationManager.locationState() + .stateIn( + viewModelScope, SharingStarted.Lazily, + WifiPermissionState.NotAvailable(WifiPermissionNotAvailableReason.NOT_AVAILABLE) + ) + + fun refreshWifiPermission() { + wifiStateManager.refreshPermission() + } + + fun refreshLocationPermission() { + locationManager.refreshPermission() + } + + fun markLocationPermissionRequested() { + locationManager.markLocationPermissionRequested() + } + + fun markWifiPermissionRequested() { + wifiStateManager.markWifiPermissionRequested() + } + + fun isWifiPermissionDeniedForever(context: Context): Boolean { + return wifiStateManager.isWifiPermissionDeniedForever(context) + } + + fun isLocationPermissionDeniedForever(context: Context): Boolean { + return locationManager.isLocationPermissionDeniedForever(context) + } +} diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml new file mode 100644 index 00000000..40e51a26 --- /dev/null +++ b/feature/nfc/src/main/res/values/strings.xml @@ -0,0 +1,17 @@ + + + + LOCATION PERMISSION REQUIRED + Location permission is required to scan for Wi-Fi networks. + + WIFI PERMISSION REQUIRED + Wi-Fi permission is required to scan for Wi-Fi networks. + WI-FI DISABLED + Wi-Fi is disabled. Please enable Wi-Fi to scan for Wi-Fi networks. + Enable Wi-Fi + WI-FI NOT AVAILABLE + Wi-Fi is not available on this device.We won\'t be able to provision the device. + + Grant Permission + Settings + \ No newline at end of file diff --git a/lib/nfc/provisioner/build.gradle.kts b/lib/nfc/provisioner/build.gradle.kts index eccf533c..be509853 100644 --- a/lib/nfc/provisioner/build.gradle.kts +++ b/lib/nfc/provisioner/build.gradle.kts @@ -11,5 +11,4 @@ android { dependencies { implementation(libs.nordic.ble.ktx) implementation(libs.nordic.ble.common) - api(project(":lib:softap:provisioner")) } \ No newline at end of file diff --git a/lib/nfc/provisioner/src/main/AndroidManifest.xml b/lib/nfc/provisioner/src/main/AndroidManifest.xml index 26d0816b..1d26c87a 100644 --- a/lib/nfc/provisioner/src/main/AndroidManifest.xml +++ b/lib/nfc/provisioner/src/main/AndroidManifest.xml @@ -1,19 +1,2 @@ - - - - - - - - - - \ No newline at end of file + \ No newline at end of file From 135feaf8c9024d99b4c6eadda9d72fc743bf3484 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 10 May 2024 16:17:55 +0200 Subject: [PATCH 04/78] wifi permission state --- .../nfc/permission/wifi/WifiStateManager.kt | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/wifi/WifiStateManager.kt diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/wifi/WifiStateManager.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/wifi/WifiStateManager.kt new file mode 100644 index 00000000..93217685 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/wifi/WifiStateManager.kt @@ -0,0 +1,85 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.wifi + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.RECEIVER_EXPORTED +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.LocalDataProvider +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.PermissionUtils +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.WifiPermissionNotAvailableReason +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.utils.WifiPermissionState +import javax.inject.Inject +import javax.inject.Singleton + +private const val REFRESH_PERMISSIONS = + "no.nordicsemi.android.common.permission.REFRESH_WIFI_PERMISSIONS" + +@Singleton +class WifiStateManager @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val dataProvider = LocalDataProvider(context) + private val utils = PermissionUtils(context, dataProvider) + + @SuppressLint("WrongConstant") + fun wifiState() = callbackFlow { + trySend(getBluetoothPermissionState()) + + val wifiStateChangeHandler = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + trySend(getBluetoothPermissionState()) + } + } + val filter = IntentFilter().apply { + addAction(BluetoothAdapter.ACTION_STATE_CHANGED) + addAction(REFRESH_PERMISSIONS) + } + + ContextCompat.registerReceiver( + context, + wifiStateChangeHandler, + filter, + RECEIVER_EXPORTED + ) + + awaitClose { + context.unregisterReceiver(wifiStateChangeHandler) + } + } + + fun refreshPermission() { + val intent = Intent(REFRESH_PERMISSIONS) + context.sendBroadcast(intent) + } + + fun markWifiPermissionRequested() { + dataProvider.wifiPermissionRequested = true + } + + fun isWifiPermissionDeniedForever(context: Context): Boolean { + return utils.isWifiPermissionDeniedForever(context) + } + + private fun getBluetoothPermissionState() = when { + !utils.isWifiAvailable -> WifiPermissionState.NotAvailable( + WifiPermissionNotAvailableReason.NOT_AVAILABLE + ) + + !utils.areNecessaryWifiPermissionsGranted -> WifiPermissionState.NotAvailable( + WifiPermissionNotAvailableReason.PERMISSION_REQUIRED + ) + + !utils.isWifiEnabled -> WifiPermissionState.NotAvailable( + WifiPermissionNotAvailableReason.DISABLED + ) + + else -> WifiPermissionState.Available + } +} From 03149280d9158bbf32a5922c5c7dc8f66f26eff7 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 10 May 2024 16:33:47 +0200 Subject: [PATCH 05/78] scan available wifi networks --- .../nfc/viemodel/NfcProvisioningViewModel.kt | 70 ++-- .../nfc/viemodel/NfcProvisioningViewState.kt | 31 ++ .../feature/nfc/view/NfcDestinations.kt | 7 +- .../feature/nfc/view/NfcProvisioningScreen.kt | 313 ++++++++++-------- .../feature/nfc/view/WifiScannerView.kt | 108 ++++++ feature/nfc/src/main/res/values/strings.xml | 9 + .../softap/view/ProvisionOverWifiSection.kt | 19 +- feature/ui/src/main/res/values/strings.xml | 16 + .../provisioner/nfc/WifiManagerRepository.kt | 79 +++-- ...dule.kt => WifiManagerRepositoryModule.kt} | 14 +- .../provisioner/nfc/domain/NetworkState.kt | 23 ++ 11 files changed, 460 insertions(+), 229 deletions(-) create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewState.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt rename lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/{WifiModule.kt => WifiManagerRepositoryModule.kt} (51%) create mode 100644 lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/NetworkState.kt diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt index adbdbee4..4fb90b8f 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt @@ -1,70 +1,62 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel -import android.util.Log +import android.os.Build +import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.wifi.provisioner.nfc.NfcManager import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import javax.inject.Inject +@RequiresApi(Build.VERSION_CODES.M) @HiltViewModel internal class NfcProvisioningViewModel @Inject constructor( private val navigator: Navigator, - private val nfcManager: NfcManager, private val wifiManager: WifiManagerRepository, ) : ViewModel() { + private val _viewState = MutableStateFlow(NfcProvisioningViewState()) + val viewState = _viewState.asStateFlow() + /** + * Handles the events from the UI. + */ fun onEvent(event: NfcProvisioningViewEvent) { when (event) { is OnScanClickEvent -> { - // TODO: Implement provision click event // Navigate to the Scanning screen. - listSsids() + _viewState.value = _viewState.value.copy( + view = Scan( + networkState = Loading(), + ) + ) + scanAvailableWifiNetworks() } OnBackClickEvent -> navigator.navigateUp() } } - private fun listSsids() { - val handler = CoroutineExceptionHandler { _, exception -> - exception.printStackTrace() - } - viewModelScope.launch(handler) { - try { - val result = wifiManager.startWifiScan() - wifiManager.wifiNetworks.onEach { scanResults -> - Log.d("AAA", "WifiNetworks: $scanResults") - scanResults.onEach { - Log.d("AAA", "ScanResult: $it") - } - }.launchIn(viewModelScope) - Log.d("AAA", "listSsids: $result") - } catch (e: Exception) { - e.printStackTrace() - Log.e("AAA", "Error: ${e.message}") - } - /*val result = nfcManager.listSsids() - Log.d("AAAA", "Results: ${result.results}") - result.results.forEach { - it.wifiInfo?.takeIf { wifiInfo -> - wifiInfo.ssid == "OnHub" - }?.let { wifiInfo -> - Log.d("AAAA", "Found the device!") - val wifiConfig = WifiConfigDomain( - info = wifiInfo, - passphrase = "newbird379", + /** + * Scans for available Wi-Fi networks. + */ + private fun scanAvailableWifiNetworks() { + try { + wifiManager.onScan() + wifiManager.networkState.onEach { scanResults -> + _viewState.value = _viewState.value.copy( + view = Scan( + networkState = scanResults, ) - Log.d("AAAA", "WifiConfig: $wifiConfig") - return@forEach - } - }*/ + ) + }.launchIn(viewModelScope) + } catch (e: Exception) { + e.printStackTrace() } } } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewState.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewState.kt new file mode 100644 index 00000000..a985125a --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewState.kt @@ -0,0 +1,31 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel + +import android.net.wifi.ScanResult +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading +import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState + +/** + * NfcProvisioningViewState is a data class that holds the state of the NFC provisioning screen. + * @param view The current view state of the NFC provisioning screen. + */ +internal data class NfcProvisioningViewState( + val view: NfcProvisioningView = Home, +) + +/** + * NfcProvisioningView is a sealed interface that holds the different view states of the NFC provisioning screen. + */ +internal sealed interface NfcProvisioningView + +/** + * Home is a data object that represents the home screen of the NFC provisioning screen. + */ +internal data object Home : NfcProvisioningView + +/** + * Scan is a data class that represents the scanning screen of the NFC provisioning screen. + * @param networkState The network state of the scanning screen. + */ +internal data class Scan( + val networkState: NetworkState> = Loading(), +) : NfcProvisioningView diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcDestinations.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcDestinations.kt index ed5a0b85..d6dc144e 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcDestinations.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcDestinations.kt @@ -1,6 +1,7 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.view import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.runtime.getValue import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -10,17 +11,15 @@ import no.nordicsemi.android.common.navigation.defineDestination import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewModel import no.nordicsemi.kotlin.wifi.provisioner.feature.common.WifiData - val NfcProvisionerDestinationId = createSimpleDestination("nfc-provider-destination") val WiFiAccessPointsDestinationIdForNfc = createDestination( name = "wifi-access-points-destination2" ) +@RequiresApi(Build.VERSION_CODES.M) val NfcProvisionerDestinations = listOf( defineDestination(NfcProvisionerDestinationId) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - NfcProvisioningScreen() - } + NfcProvisioningScreen() }, defineDestination(WiFiAccessPointsDestinationIdForNfc) { val viewModel = hiltViewModel() diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt index 2a102587..0abc3452 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt @@ -1,5 +1,7 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.view +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -34,175 +36,208 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import no.nordicsemi.android.common.theme.view.NordicAppBar +import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireLocationForWifi +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireWifi +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.Home import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.NfcProvisioningViewEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.NfcProvisioningViewModel import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.OnBackClickEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.OnScanClickEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.Scan +@RequiresApi(Build.VERSION_CODES.M) @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun NfcProvisioningScreen() { val viewModel: NfcProvisioningViewModel = hiltViewModel() + val viewState by viewModel.viewState.collectAsStateWithLifecycle() val onEvent: (NfcProvisioningViewEvent) -> Unit = { viewModel.onEvent(it) } - var openAddWifiDialog by remember { mutableStateOf(false) } - Column( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 56.dp) - ) { - NordicAppBar( - text = "Wifi Provisioning over NFC", - showBackButton = true, - onNavigationButtonClick = { onEvent(OnBackClickEvent) } - ) - // Content - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .weight(1f) - .padding(8.dp) - ) { - // Show an option to enter the WiFi credentials manually. - OutlinedCard( + RequireWifi { + RequireLocationForWifi { + Column( modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .clickable { - openAddWifiDialog = true - } - + .fillMaxSize() + .padding(bottom = 56.dp) ) { - // TODO: Once this is merged with NFC app, replace this with the one from NFC app. - NfcRecordOutlinedCardItem( - headline = "Enter WiFi credentials manually", - description = { - Text( - text = "Add WiFi credentials manually.", - style = MaterialTheme.typography.bodyMedium, - ) - }, - icon = Icons.Default.Wifi, - ) { - Spacer(Modifier.weight(1f)) - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - modifier = Modifier - .clip(CircleShape) - .clickable { - openAddWifiDialog = true - } - .padding(8.dp) - ) + NordicAppBar( + text = stringResource(id = R.string.wifi_provision_over_nfc_appbar), + showBackButton = true, + onNavigationButtonClick = { onEvent(OnBackClickEvent) } + ) + // Show Content. + when (val s = viewState.view) { + Home -> { + // Show the home screen. + NfcProvisioningHomeView(onEvent) + } + is Scan -> { + // Show the scanning screen. + WifiScannerView(s.networkState, onEvent) + } } } + } + } +} - // Show an option to search for a WiFi network. - OutlinedCard( - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .clickable { - // TODO: Show a list of WiFi networks. - - onEvent(OnScanClickEvent) - } +@Composable +private fun NfcProvisioningHomeView( + onEvent: (NfcProvisioningViewEvent) -> Unit +) { + var openAddWifiDialog by remember { mutableStateOf(false) } + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.Companion + .padding(8.dp) + ) { + // Show an option to enter the WiFi credentials manually. + OutlinedCard( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .clickable { + openAddWifiDialog = true + } - ) { - NfcRecordOutlinedCardItem( - headline = "Search for WiFi networks", - description = { - Text( - text = "Search for available WiFi networks.", - style = MaterialTheme.typography.bodyMedium, - ) - }, - icon = Icons.Default.WifiFind, - ) { - Spacer(Modifier.weight(1f)) - Icon( - imageVector = Icons.Default.Search, - contentDescription = null, - modifier = Modifier - .clip(CircleShape) - .clickable { - // TODO: Show a list of WiFi networks. - onEvent(OnScanClickEvent) - } - .padding(8.dp) + ) { + NfcRecordOutlinedCardItem( + headline = stringResource(id = R.string.enter_wifi_credentials), + description = { + Text( + text = stringResource(id = R.string.enter_wifi_credentials_des), + style = MaterialTheme.typography.bodyMedium, ) + }, + icon = Icons.Default.Wifi, + ) { + Spacer(Modifier.weight(1f)) + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .clickable { + openAddWifiDialog = true + } + .padding(8.dp) + ) + } + } + + // Show an option to search for a WiFi network. + OutlinedCard( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .clickable { + onEvent(OnScanClickEvent) } + ) { + NfcRecordOutlinedCardItem( + headline = stringResource(id = R.string.search_for_wifi_networks), + description = { + Text( + text = stringResource(id = R.string.search_for_wifi_networks_des), + style = MaterialTheme.typography.bodyMedium, + ) + }, + icon = Icons.Default.WifiFind, + ) { + Spacer(Modifier.weight(1f)) + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .clickable { onEvent(OnScanClickEvent) } + .padding(8.dp) + ) } - if (openAddWifiDialog) { - // Open a dialog to enter the WiFi credentials manually. - AlertDialog( - onDismissRequest = { }, - icon = { - Icon(imageVector = Icons.Outlined.Wifi, contentDescription = null) - }, - title = { - Text( - text = "Setup WiFi", - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge - ) - }, - text = { - Column { - var ssid by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - var showPassword by remember { mutableStateOf(false) } - Text( - text = "Setup WiFi", - modifier = Modifier.fillMaxWidth(), - style = MaterialTheme.typography.bodyMedium - ) - Spacer(modifier = Modifier.size(size = 16.dp)) - OutlinedTextField(value = ssid, onValueChange = { ssid = it }) - Spacer(modifier = Modifier.size(size = 8.dp)) - OutlinedTextField( - value = password, - onValueChange = { password = it }, - visualTransformation = if (showPassword) - VisualTransformation.None - else - PasswordVisualTransformation(), - trailingIcon = { - IconButton(onClick = { showPassword = !showPassword }) { - Icon( - imageVector = if (!showPassword) - Icons.Outlined.Visibility - else Icons.Outlined.VisibilityOff, - contentDescription = null - ) - } - } + } + + if (openAddWifiDialog) { + // Open a dialog to enter the WiFi credentials manually. + OpenAddWifiDialog( + onClick = { + openAddWifiDialog = false + } + ) + } + } +} + +@Composable +private fun OpenAddWifiDialog( + onClick: () -> Unit, +) { + AlertDialog( + onDismissRequest = { }, + icon = { + Icon(imageVector = Icons.Outlined.Wifi, contentDescription = null) + }, + title = { + Text( + text = "Setup WiFi", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + }, + text = { + Column { + var ssid by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var showPassword by remember { mutableStateOf(false) } + Text( + text = "Setup WiFi", + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.size(size = 16.dp)) + OutlinedTextField(value = ssid, onValueChange = { ssid = it }) + Spacer(modifier = Modifier.size(size = 8.dp)) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + visualTransformation = if (showPassword) + VisualTransformation.None + else + PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { showPassword = !showPassword }) { + Icon( + imageVector = if (!showPassword) + Icons.Outlined.Visibility + else Icons.Outlined.VisibilityOff, + contentDescription = null ) } - }, - dismissButton = { - TextButton( - onClick = { - openAddWifiDialog = false - } - ) { Text(text = "Cancel") } - }, - confirmButton = { - TextButton( - onClick = { - openAddWifiDialog = false - // TODO: Connect to the WiFi network. - } - ) { Text(text = "Confirm") } } ) } + }, + dismissButton = { + TextButton( + onClick = { + onClick() + } + ) { Text(text = "Cancel") } + }, + confirmButton = { + TextButton( + onClick = { + onClick() + // TODO: Connect to the WiFi network. + } + ) { Text(text = "Confirm") } } - } + ) } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt new file mode 100644 index 00000000..284a823c --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -0,0 +1,108 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.view + +import android.net.wifi.ScanResult +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.NfcProvisioningViewEvent +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading +import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success + +@Composable +internal fun WifiScannerView( + scanningState: NetworkState>, + onEvent: (NfcProvisioningViewEvent) -> Unit +) { + // Show the scanning screen. + when (scanningState) { + is Error -> { + // Show the error message. + Column { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = "Error occurred while scanning for networks.") + Text(text = scanningState.t.message ?: "Unknown error occurred.") + } + } + } + + is Loading -> { + // Show the loading indicator. + Column { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.padding(16.dp) + ) + } + } + } + + is Success -> { + // Show the list of available networks. + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + scanningState.data.forEach { network -> + // Display the network. + OutlinedCard( + modifier = Modifier.fillMaxWidth(), + onClick = { /*TODO*/ }) { + // Display the network details. + Column( + modifier = Modifier.padding(8.dp) + ) { + Text( + text = network.SSID, + style = MaterialTheme.typography.titleMedium + ) + Row { + // Display the network name. + // Display the network signal strength. + Text( + text = "Signal Strength: ${network.level} dBm", + style = MaterialTheme.typography.bodySmall + ) + Text( + text = network.BSSID, + style = MaterialTheme.typography.bodySmall + ) + } + // Display the network capabilities. + Text(text = "Capabilities") + Text( + text = network.capabilities, + style = MaterialTheme.typography.bodySmall + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index 40e51a26..028bcbc7 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -1,5 +1,11 @@ + Wifi Provisioning over NFC + Enter Wi-Fi credentials + Enter the Wi-Fi credentials to provision the device. + Search for Wi-Fi networks + Search for Wi-Fi networks to provision the device. + LOCATION PERMISSION REQUIRED Location permission is required to scan for Wi-Fi networks. @@ -14,4 +20,7 @@ Grant Permission Settings + + Error occurred while scanning for networks. + Unknown error occurred. \ No newline at end of file diff --git a/feature/softap/src/main/java/no/nordicsemi/android/wifi/provisioner/softap/view/ProvisionOverWifiSection.kt b/feature/softap/src/main/java/no/nordicsemi/android/wifi/provisioner/softap/view/ProvisionOverWifiSection.kt index 5812bafa..1d05d444 100644 --- a/feature/softap/src/main/java/no/nordicsemi/android/wifi/provisioner/softap/view/ProvisionOverWifiSection.kt +++ b/feature/softap/src/main/java/no/nordicsemi/android/wifi/provisioner/softap/view/ProvisionOverWifiSection.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.unit.dp import no.nordicsemi.android.wifi.provisioner.feature.softap.R import no.nordicsemi.android.wifi.provisioner.ui.view.section.SectionTitle - @Composable fun ProvisionOverWifiSection(onClick: () -> Unit) { OutlinedCard( @@ -36,4 +35,22 @@ fun ProvisionOverWifiSection(onClick: () -> Unit) { ) } } +} + +@Composable +fun ProvisionOverNfc(onClick: () -> Unit) { + OutlinedCard( + modifier = Modifier + .padding(all = 8.dp) + .clickable(onClick = onClick) + ) { + Column(modifier = Modifier.padding(all = 16.dp)) { + SectionTitle(text = stringResource(R.string.provision_over_nfc)) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = stringResource(R.string.provision_over_nfc_rationale), + style = MaterialTheme.typography.bodyMedium + ) + } + } } \ No newline at end of file diff --git a/feature/ui/src/main/res/values/strings.xml b/feature/ui/src/main/res/values/strings.xml index f53f1fd5..3ba6bd9d 100644 --- a/feature/ui/src/main/res/values/strings.xml +++ b/feature/ui/src/main/res/values/strings.xml @@ -87,4 +87,20 @@ Select Wi-Fi Persistent storage + + Provision over Bluetooth LE + This mode allows provisioning a nRF700x device to a + Wi-Fi network by providing the Wi-Fi credentials, directly connecting to the + device using Bluetooth LE + + Provision over Wi-Fi + This mode allows provisioning a nRF700x device to a + Wi-Fi network by providing the Wi-Fi credentials over Wi-Fi by directly connecting to the + Soft AP advertised by the device. + + + Provision over NFC + This mode allows provisioning a nRF700x device to a + Wi-Fi network by providing the Wi-Fi credentials via NFC. + \ No newline at end of file diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt index 143c5730..2eb0573d 100644 --- a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt @@ -1,63 +1,62 @@ package no.nordicsemi.android.wifi.provisioner.nfc -import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.wifi.ScanResult import android.net.wifi.WifiManager -import android.util.Log -import dagger.hilt.android.qualifiers.ApplicationContext +import android.os.Build +import androidx.annotation.RequiresApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import no.nordicsemi.android.wifi.provisioner.softap.SoftApManager -import javax.inject.Inject -import javax.inject.Singleton +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading +import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success -@Singleton -class NfcManager @Inject constructor( - private val softApManager: SoftApManager, +@RequiresApi(Build.VERSION_CODES.M) +@SuppressWarnings("MissingPermission") +class WifiManagerRepository( + context: Context, ) { + private var wifiManager: WifiManager = + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + private val _networkState = MutableStateFlow>>(Loading()) + val networkState = _networkState.asStateFlow() /** - * Lists the SSIDs scanned by the nRF7002 device + * Broadcast receiver to receive the scan results. */ - suspend fun listSsids() = softApManager.listSsids() -} - -@Singleton -@SuppressLint("MissingPermission") -class WifiManagerRepository @Inject constructor( - private val wifiManager: WifiManager, - @ApplicationContext private val context: Context, -) { - private val _wifiNetworks = MutableStateFlow>(emptyList()) - val wifiNetworks = _wifiNetworks.asStateFlow() - - init { - if (!wifiManager.isWifiEnabled) { - wifiManager.isWifiEnabled = true - } - val wifiInfo = wifiManager.connectionInfo?.let { - Log.d("AAA", "Connected to: ${it.ssid}") - } - // Register BroadcastReceiver to receive scan results - val wifiScanReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) { - _wifiNetworks.value = wifiManager.scanResults - Log.d("AAA", "onReceive: ${wifiManager.scanResults}") + private val wifiScanReceiver = object : BroadcastReceiver() { + @RequiresApi(Build.VERSION_CODES.M) + override fun onReceive(context: Context, intent: Intent) { + try { + val success = + intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false) + if (!success) { + wifiManager.startScan() } + val results = wifiManager.scanResults + _networkState.value = Success(results) + } catch (e: Exception) { + e.printStackTrace() + _networkState.value = Error(e) } } - context.registerReceiver( - wifiScanReceiver, - IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) - ) } - fun startWifiScan() { + init { + // Register the broadcast receiver + val intentFilter = IntentFilter() + intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) + context.registerReceiver(wifiScanReceiver, intentFilter) + } + + /** + * This method is used to start the wifi scan. + */ + fun onScan() { wifiManager.startScan() } } \ No newline at end of file diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiModule.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt similarity index 51% rename from lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiModule.kt rename to lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt index 1321701a..ff46f535 100644 --- a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiModule.kt +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt @@ -1,21 +1,23 @@ package no.nordicsemi.android.wifi.provisioner.nfc.di import android.content.Context -import android.net.wifi.WifiManager -import android.nfc.NfcAdapter +import android.os.Build +import androidx.annotation.RequiresApi import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -object WifiModule { +object WifiManagerRepositoryModule { + + @RequiresApi(Build.VERSION_CODES.M) @Provides @Singleton - fun provideWifiManager(@ApplicationContext context: Context): WifiManager { - return context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager - } + fun provideWifiManagerRepositoryModule(@ApplicationContext context: Context) = + WifiManagerRepository(context = context) } \ No newline at end of file diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/NetworkState.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/NetworkState.kt new file mode 100644 index 00000000..ebce9a19 --- /dev/null +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/NetworkState.kt @@ -0,0 +1,23 @@ +package no.nordicsemi.android.wifi.provisioner.nfc.domain + +/** + * NetworkState is a sealed interface that holds the different network states. + */ +sealed interface NetworkState + +/** + * Success is a data class that represents the success state of the network. + * @param data The data that is returned from the network. + */ +data class Success(val data: T) : NetworkState + +/** + * Error is a data class that represents the error state of the network. + * @param t The throwable that is returned from the network. + */ +data class Error(val t: Throwable) : NetworkState + +/** + * Loading is a class that represents the loading state of the network. + */ +class Loading : NetworkState From 16e6029ef5fa5c99ca99e4878264073ce4afe345 Mon Sep 17 00:00:00 2001 From: hiar Date: Mon, 13 May 2024 16:35:31 +0200 Subject: [PATCH 06/78] Show wifi list --- .../feature/nfc/data/ScanResultSecurity.kt | 26 +++++ .../nfc/viemodel/NfcProvisioningViewEvent.kt | 7 ++ .../nfc/viemodel/NfcProvisioningViewModel.kt | 3 + .../feature/nfc/view/RssiIconView.kt | 78 +++++++++++++ .../feature/nfc/view/WifiScannerView.kt | 109 ++++++++++++------ 5 files changed, 185 insertions(+), 38 deletions(-) create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/ScanResultSecurity.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/RssiIconView.kt diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/ScanResultSecurity.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/ScanResultSecurity.kt new file mode 100644 index 00000000..faa1726e --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/ScanResultSecurity.kt @@ -0,0 +1,26 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.data + +import android.net.wifi.ScanResult + +// Constants used for different security types +const val WPA2 = "WPA2" +const val WPA = "WPA" +const val WEP = "WEP" +const val OPEN = "Open" +const val IEEE8021X = "IEEE8021X" +const val WPA_EAP = "WPA-EAP" + +/** + * @return The security of a given [ScanResult]. + */ +fun getScanResultSecurity(scanResult: ScanResult): String { + val cap = scanResult.capabilities + val securityModes = arrayOf(WEP, WPA, WPA2, WPA_EAP, IEEE8021X) + for (i in securityModes.indices.reversed()) { + if (cap.contains(securityModes[i])) { + return securityModes[i] + } + } + + return OPEN +} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt index 332ec04f..bf74a650 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt @@ -1,5 +1,7 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel +import android.net.wifi.ScanResult + /** * A sealed class to represent the events that can be triggered from the UI. */ @@ -14,3 +16,8 @@ internal data object OnScanClickEvent : NfcProvisioningViewEvent * Event triggered when the back button is clicked. */ internal data object OnBackClickEvent: NfcProvisioningViewEvent + +/** + * Event triggered when the wifi network is selected. + */ +internal data class OnNetworkSelectedEvent(val network: ScanResult) : NfcProvisioningViewEvent diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt index 4fb90b8f..2e2d9eba 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt @@ -39,6 +39,9 @@ internal class NfcProvisioningViewModel @Inject constructor( } OnBackClickEvent -> navigator.navigateUp() + is OnNetworkSelectedEvent -> { + // Navigate to connect to the selected network. + } } } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/RssiIconView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/RssiIconView.kt new file mode 100644 index 00000000..d37b6eb2 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/RssiIconView.kt @@ -0,0 +1,78 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.view + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.NetworkWifi +import androidx.compose.material.icons.filled.NetworkWifi1Bar +import androidx.compose.material.icons.filled.NetworkWifi2Bar +import androidx.compose.material.icons.filled.NetworkWifi3Bar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.common.theme.NordicTheme +import no.nordicsemi.android.common.theme.R +import no.nordicsemi.android.common.theme.nordicBlue + +// Constants used for different signal strengths +private const val FAIR_RSSI = -70 +private const val GOOD_RSSI = -60 +private const val EXCELLENT_RSSI = -50 + +/** + * A function to get the WiFi icon based on the RSSI value. + * The icon is selected based on the following criteria: + * - Weak signal: RSSI < -70 dBm + * - Fair signal: -70 dBm <= RSSI < -60 dBm + * - Good signal: -60 dBm <= RSSI < -50 dBm + * - Excellent signal: RSSI >= -50 dBm + * + * Selection criteria was inspired by [this](https://www.netspotapp.com/wifi-signal-strength/what-is-rssi-level.html) article. + * + * @param rssi The RSSI value. + */ +internal fun getWiFiIcon(rssi: Int): ImageVector { + return when (rssi) { + in Int.MIN_VALUE..FAIR_RSSI -> Icons.Default.NetworkWifi3Bar // Weak signal + in FAIR_RSSI..GOOD_RSSI -> Icons.Default.NetworkWifi2Bar // Fair signal + in GOOD_RSSI..EXCELLENT_RSSI -> Icons.Default.NetworkWifi1Bar // Good signal + else -> Icons.Default.NetworkWifi// Excellent signal + } +} + +/** + * A composable function to display the RSSI icon and value. + * + * @param rssi The RSSI value. + */ +@Composable +internal fun RssiIconView(rssi: Int) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Image( + imageVector = getWiFiIcon(rssi), + contentDescription = null, + modifier = Modifier.size(28.dp), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.nordicBlue) + ) + Text( + text = stringResource(id = R.string.dbm, rssi), + style = MaterialTheme.typography.labelSmall + ) + } +} + +@Preview +@Composable +private fun RssiIconViewPreview() { + NordicTheme { + RssiIconView(-50) + } +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index 284a823c..31cc88d5 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -1,6 +1,8 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.view import android.net.wifi.ScanResult +import android.net.wifi.WifiSsid +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,19 +13,30 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.getScanResultSecurity import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.NfcProvisioningViewEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.OnNetworkSelectedEvent import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success +/** + * A composable function to display the list of available networks. + * + * @param scanningState The state of the network scanning. + * @param onEvent The event callback. + */ @Composable internal fun WifiScannerView( scanningState: NetworkState>, @@ -39,7 +52,7 @@ internal fun WifiScannerView( .fillMaxSize(), contentAlignment = Alignment.Center ) { - Text(text = "Error occurred while scanning for networks.") + Text(text = stringResource(id = R.string.error_while_scanning)) Text(text = scanningState.t.message ?: "Unknown error occurred.") } } @@ -66,43 +79,63 @@ internal fun WifiScannerView( modifier = Modifier .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) ) { - scanningState.data.forEach { network -> - // Display the network. - OutlinedCard( - modifier = Modifier.fillMaxWidth(), - onClick = { /*TODO*/ }) { - // Display the network details. - Column( - modifier = Modifier.padding(8.dp) - ) { - Text( - text = network.SSID, - style = MaterialTheme.typography.titleMedium - ) - Row { - // Display the network name. - // Display the network signal strength. - Text( - text = "Signal Strength: ${network.level} dBm", - style = MaterialTheme.typography.bodySmall - ) - Text( - text = network.BSSID, - style = MaterialTheme.typography.bodySmall - ) - } - // Display the network capabilities. - Text(text = "Capabilities") - Text( - text = network.capabilities, - style = MaterialTheme.typography.bodySmall - ) - } - } - } + WifiList(scanningState.data, onEvent) } } } -} \ No newline at end of file +} + +/** + * A composable function to display the list of available networks. + * + * @param networks The list of available networks. + * @param onEvent The event callback. + */ +@Composable +internal fun WifiList( + networks: List, + onEvent: (NfcProvisioningViewEvent) -> Unit +) { + networks.forEach { network -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .clickable { onEvent(OnNetworkSelectedEvent(network)) }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + RssiIconView(network.level) + Column( + modifier = Modifier.padding(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + // Display the SSID of the access point + Text( + text = getSSid(network.wifiSsid), + style = MaterialTheme.typography.bodyLarge + ) + // Display the address of the access point. + Text( + text = network.BSSID.uppercase(), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(0.7f) + ) + // Display the security type of the access point. + Text( + text = getScanResultSecurity(network), + modifier = Modifier.alpha(0.7f) + ) + } + } + HorizontalDivider() + } +} + +/** + * Returns the SSID of the network from given [WifiSsid]. + */ +fun getSSid(wifiSsid: WifiSsid?): String { + return wifiSsid.toString().replace("\"", "") +} From da28674b92afb3c6706f739a683d65bd84eec44c Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 14 May 2024 16:17:24 +0200 Subject: [PATCH 07/78] nfc permission --- feature/nfc/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/feature/nfc/build.gradle.kts b/feature/nfc/build.gradle.kts index df1903aa..097df420 100644 --- a/feature/nfc/build.gradle.kts +++ b/feature/nfc/build.gradle.kts @@ -17,4 +17,5 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.compose) implementation(project(":feature:common")) api(project(":lib:nfc:provisioner")) + implementation(libs.nordic.permissions.nfc) } \ No newline at end of file From 9b6f3ac21faa69200df486d3e2358def42cf4d53 Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 14 May 2024 16:17:40 +0200 Subject: [PATCH 08/78] nfc adapter --- .../wifi/provisioner/nfc/di/NfcAdapter.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt new file mode 100644 index 00000000..a0d9aa41 --- /dev/null +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt @@ -0,0 +1,29 @@ +package no.nordicsemi.android.wifi.provisioner.nfc.di + +import android.content.Context +import android.nfc.NfcAdapter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import no.nordicsemi.android.wifi.provisioner.nfc.NfcManagerForWifi +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NfcAdapterModule { + + @Provides + @Singleton + fun provideNfcAdapter(@ApplicationContext context: Context): NfcAdapter? { + return NfcAdapter.getDefaultAdapter(context) + } + + @Provides + @Singleton + fun provideWifiConnectionManager(@ApplicationContext context: Context) = + NfcManagerForWifi( + nfcAdapter = provideNfcAdapter(context)!! + ) +} \ No newline at end of file From 935e156fcefd3309518db6391dcd0b03734caf4a Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 14 May 2024 16:18:05 +0200 Subject: [PATCH 09/78] write wifi record to tag --- .../wifi/provisioner/nfc/NfcManagerForWifi.kt | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt new file mode 100644 index 00000000..72ab4977 --- /dev/null +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt @@ -0,0 +1,104 @@ +package no.nordicsemi.android.wifi.provisioner.nfc + +import android.app.Activity +import android.nfc.NdefMessage +import android.nfc.NfcAdapter +import android.nfc.Tag +import android.nfc.tech.Ndef +import android.nfc.tech.NdefFormatable +import javax.inject.Inject +import javax.inject.Singleton + +enum class NfcFlags(val value: Int) { + NFC_A(NfcAdapter.FLAG_READER_NFC_A), + NFC_B(NfcAdapter.FLAG_READER_NFC_B), + NFC_F(NfcAdapter.FLAG_READER_NFC_F), + NFC_V(NfcAdapter.FLAG_READER_NFC_V), + NFC_BARCODE(NfcAdapter.FLAG_READER_NFC_BARCODE), +} + +/** The Nfc reader flags for the NfcAdapter. */ +private val flags = setOf( + NfcFlags.NFC_A, + NfcFlags.NFC_B, + NfcFlags.NFC_F, + NfcFlags.NFC_V, + NfcFlags.NFC_BARCODE +).fold(0) { acc, flag -> acc or flag.value } + +/** + * A class that manages the NFC adapter for the wifi provisioning. + */ +@Singleton +class NfcManagerForWifi @Inject constructor( + private val nfcAdapter: NfcAdapter?, +) { + private var ndefMessage: NdefMessage? = null + + /** + * Handles the NFC tap event. + * @param activity the activity. + * @param message the Ndef message. + */ + fun onNfcTap(activity: Activity, message: NdefMessage) { + ndefMessage = message + nfcAdapter?.takeIf { it.isEnabled }?.let { + val readerFlag = getReaderFlag() + it.enableReaderMode(activity, ::onTagDiscovered, readerFlag, null) + } + } + + /** + * Callback when tag is discovered. + * @param tag the discovered tag. + */ + private fun onTagDiscovered(tag: Tag?) { + try { + tag?.let { + ndefMessage?.let { + if (tag.techList.contains(Ndef::class.java.name)) { + // Write the Ndef message to the tag. + val ndef = Ndef.get(tag) + try { + ndef.connect() + ndef.writeNdefMessage(it) + ndef.close() + } catch (e: Exception) { + e.printStackTrace() + } + } else if (tag.techList.contains(NdefFormatable::class.java.name)) { + // Format the tag and write the Ndef message. + val ndefFormatable = NdefFormatable.get(tag) + try { + ndefFormatable.connect() + ndefFormatable.format(it) + ndefFormatable.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + + } + + /** + * Returns each NfcFlags and plays sounds while scanning if the isSoundOn parameter is set to ON. + */ + private fun getReaderFlag(): Int { + val soundFlag = 0 + return flags or soundFlag + } + + + /** + * Pauses the NFC reader. + * @param activity the activity. + */ + fun onPause(activity: Activity) { + nfcAdapter?.disableReaderMode(activity) + } +} \ No newline at end of file From 5b50a24fd66b0fd4e13e52ba12726b49cf6fbcba Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 14 May 2024 16:18:20 +0200 Subject: [PATCH 10/78] nfc scan --- .../nfc/viemodel/NfcManagerViewModel.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcManagerViewModel.kt diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcManagerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcManagerViewModel.kt new file mode 100644 index 00000000..c55f6bde --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcManagerViewModel.kt @@ -0,0 +1,28 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel + +import android.app.Activity +import android.nfc.NdefMessage +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import no.nordicsemi.android.wifi.provisioner.nfc.NfcManagerForWifi +import javax.inject.Inject + +/** + * ViewModel for the NFC manager. + */ +@HiltViewModel +internal class NfcManagerViewModel @Inject constructor( + private val nfcManagerForWifi: NfcManagerForWifi, +) : ViewModel() { + + fun onScan(activity: Activity, ndefMessage: NdefMessage) { + nfcManagerForWifi.onNfcTap( + activity = activity, + message = ndefMessage + ) + } + + fun onPause(activity: Activity) { + nfcManagerForWifi.onPause(activity) + } +} \ No newline at end of file From 99f0f6e97ad1b3615323a2c04bbf55a569742e2f Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 14 May 2024 16:18:35 +0200 Subject: [PATCH 11/78] nf provisioning --- .../feature/nfc/view/NfcProvisioningScreen.kt | 141 +++++++++++++++++- 1 file changed, 139 insertions(+), 2 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt index 0abc3452..7934c4a4 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt @@ -1,5 +1,8 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.view +import android.app.Activity +import android.net.wifi.ScanResult +import android.nfc.NdefMessage import android.os.Build import androidx.annotation.RequiresApi import androidx.compose.foundation.clickable @@ -30,12 +33,14 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation @@ -43,15 +48,20 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.common.permissions.nfc.RequireNfc import no.nordicsemi.android.common.theme.view.NordicAppBar import no.nordicsemi.android.wifi.provisioner.feature.nfc.R import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireLocationForWifi import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireWifi +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.AskForPassword import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.Home +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.NfcManagerViewModel import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.NfcProvisioningViewEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.NfcProvisioningViewModel import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.OnBackClickEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.OnPasswordConfirmedEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.OnScanClickEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.Provisioning import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.Scan @RequiresApi(Build.VERSION_CODES.M) @@ -61,6 +71,8 @@ internal fun NfcProvisioningScreen() { val viewModel: NfcProvisioningViewModel = hiltViewModel() val viewState by viewModel.viewState.collectAsStateWithLifecycle() val onEvent: (NfcProvisioningViewEvent) -> Unit = { viewModel.onEvent(it) } + var openPasswordDialog by remember { mutableStateOf(false) } + var scanResult: ScanResult? = null RequireWifi { RequireLocationForWifi { @@ -76,20 +88,145 @@ internal fun NfcProvisioningScreen() { ) // Show Content. when (val s = viewState.view) { - Home -> { + Home -> { // Show the home screen. - NfcProvisioningHomeView(onEvent) + NfcProvisioningHomeView(onEvent) } + is Scan -> { // Show the scanning screen. WifiScannerView(s.networkState, onEvent) } + + is AskForPassword -> { + // Show the screen to ask for the password. + openPasswordDialog = true + scanResult = s.network + } + + Provisioning -> { + // TODO: Show the provisioning screen. + // Publish the NDEF message to the tag. + ProvisioningView( + ndefMessage = viewModel.ndefMessage!! + ) + } + } + if (openPasswordDialog) { + // Open a dialog to enter the WiFi credentials manually. + scanResult?.let { result -> + PasswordDialog( + scanResult = result, + onCancelClick = { openPasswordDialog = false }, + onConfirmClick = { password, scanResult -> + openPasswordDialog = false + // Go to the next screen. + onEvent(OnPasswordConfirmedEvent(password, scanResult)) + } + ) + } } } } } } +@Composable +internal fun ProvisioningView( + ndefMessage: NdefMessage +) { + val nfcManagerVm: NfcManagerViewModel = hiltViewModel() + val context = LocalContext.current + + RequireNfc { + DisposableEffect(key1 = nfcManagerVm) { + nfcManagerVm.onScan(context as Activity, ndefMessage) + onDispose { nfcManagerVm.onPause(context) } + } + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally + ) { + Text(text = "Hold the NFC tag near the device to provision the WiFi network.") + } + } +} + +@Composable +internal fun PasswordDialog( + scanResult: ScanResult, + onCancelClick: () -> Unit, + onConfirmClick: (String, ScanResult) -> Unit, +) { + var password by remember { mutableStateOf("") } + AlertDialog( + onDismissRequest = { }, + icon = { + Icon(imageVector = Icons.Outlined.Wifi, contentDescription = null) + }, + title = { + Text( + text = "Setup WiFi", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + }, + text = { + Column { + var ssid by remember { mutableStateOf(scanResult.SSID) } + + var showPassword by remember { mutableStateOf(false) } + Text( + text = "Setup WiFi", + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.size(size = 16.dp)) + OutlinedTextField(value = ssid, onValueChange = { ssid = it }) + Spacer(modifier = Modifier.size(size = 8.dp)) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + visualTransformation = if (showPassword) + VisualTransformation.None + else + PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { showPassword = !showPassword }) { + Icon( + imageVector = if (!showPassword) + Icons.Outlined.VisibilityOff + else Icons.Outlined.Visibility, + contentDescription = null + ) + } + } + ) + } + }, + dismissButton = { + TextButton( + onClick = { + onCancelClick() + } + ) { Text(text = "Cancel") } + }, + confirmButton = { + TextButton( + onClick = { + onConfirmClick( + password, + scanResult + ) + } + ) { Text(text = "Confirm") } + } + ) +} + @Composable private fun NfcProvisioningHomeView( onEvent: (NfcProvisioningViewEvent) -> Unit From 32a7518a365357961d6a403b3c9241475450b4d5 Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 14 May 2024 16:18:49 +0200 Subject: [PATCH 12/78] provisioning view event --- .../nfc/viemodel/NfcProvisioningViewEvent.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt index bf74a650..c246c471 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt @@ -19,5 +19,18 @@ internal data object OnBackClickEvent: NfcProvisioningViewEvent /** * Event triggered when the wifi network is selected. + * + * @param network The selected network. */ internal data class OnNetworkSelectedEvent(val network: ScanResult) : NfcProvisioningViewEvent + +/** + * Event triggered when password is confirmed. + * + * @param password The password entered by the user. + * @param network The selected network. + */ +internal data class OnPasswordConfirmedEvent( + val password: String, + val network: ScanResult, + ) : NfcProvisioningViewEvent From fca430de6deed71b05831656270c3ccbd3cb49d0 Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 14 May 2024 16:19:49 +0200 Subject: [PATCH 13/78] create ndef record for wifi data --- ...{ScanResultSecurity.kt => WifiAuthType.kt} | 0 .../nfc/viemodel/NfcProvisioningViewModel.kt | 32 +++++ .../nfc/viemodel/NfcProvisioningViewState.kt | 7 ++ .../nfc/WifiConfigNdefMessageBuilder.kt | 114 ++++++++++++++++++ .../wifi/provisioner/nfc/domain/WifiData.kt | 14 +++ 5 files changed, 167 insertions(+) rename feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/{ScanResultSecurity.kt => WifiAuthType.kt} (100%) create mode 100644 lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiConfigNdefMessageBuilder.kt create mode 100644 lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/ScanResultSecurity.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt similarity index 100% rename from feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/ScanResultSecurity.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt index 2e2d9eba..e6815b1e 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt @@ -1,5 +1,6 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel +import android.nfc.NdefMessage import android.os.Build import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModel @@ -10,8 +11,10 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.wifi.provisioner.nfc.WifiConfigNdefMessageBuilder import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData import javax.inject.Inject @RequiresApi(Build.VERSION_CODES.M) @@ -19,9 +22,11 @@ import javax.inject.Inject internal class NfcProvisioningViewModel @Inject constructor( private val navigator: Navigator, private val wifiManager: WifiManagerRepository, + private val wifiConfigNdefMessageBuilder: WifiConfigNdefMessageBuilder, ) : ViewModel() { private val _viewState = MutableStateFlow(NfcProvisioningViewState()) val viewState = _viewState.asStateFlow() + var ndefMessage: NdefMessage? = null /** * Handles the events from the UI. @@ -41,6 +46,33 @@ internal class NfcProvisioningViewModel @Inject constructor( OnBackClickEvent -> navigator.navigateUp() is OnNetworkSelectedEvent -> { // Navigate to connect to the selected network. + // Ask the user to enter the password. + + // TODO: Check if the network is open or not. + // If the network is open, then connect to it directly. + // Otherwise, ask the user to enter the password. + _viewState.value = _viewState.value.copy( + view = AskForPassword( + network = event.network + ) + ) + } + + is OnPasswordConfirmedEvent -> { + // Create a NdefMessage with the network details. + ndefMessage = wifiConfigNdefMessageBuilder.createNdefMessage( + wifiNetwork = WifiData( + ssid = event.network.SSID, + password = event.password, + authType = "WPA2-PSK", // TODO: For now, we are only supporting WPA2_PSK. + ) + ) + /* wifiConfigNdefMessageBuilder.onNfcTap(activity = event.activity + ,ndefMessage)*/ + + _viewState.value = _viewState.value.copy( + view = Provisioning + ) } } } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewState.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewState.kt index a985125a..9bd8530d 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewState.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewState.kt @@ -29,3 +29,10 @@ internal data object Home : NfcProvisioningView internal data class Scan( val networkState: NetworkState> = Loading(), ) : NfcProvisioningView + +internal data class AskForPassword( + val network: ScanResult, +) : NfcProvisioningView + +internal data object Provisioning : NfcProvisioningView + diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiConfigNdefMessageBuilder.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiConfigNdefMessageBuilder.kt new file mode 100644 index 00000000..940df572 --- /dev/null +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiConfigNdefMessageBuilder.kt @@ -0,0 +1,114 @@ +package no.nordicsemi.android.wifi.provisioner.nfc + +import android.nfc.NdefMessage +import android.nfc.NdefRecord +import android.provider.ContactsContract +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData +import javax.inject.Inject +import kotlin.math.floor + +internal const val NFC_TOKEN_MIME_TYPE: String = "application/vnd.wfa.wsc" +private val CREDENTIAL_FIELD_ID = byteArrayOf(0x10, 0x0e) +private val NETWORK_IDX: ByteArray = byteArrayOf(0x10, 0x26) +private val WPS_AUTH_WPA2_PERSONAL: ByteArray = byteArrayOf(0x00, 0x20) + +private val WPS_CRYPT_AES: ByteArray = byteArrayOf(0x00, 0x08) +private val AUTH_TYPE: ByteArray = byteArrayOf(0x10, 0x03) +private val AUTH_WPA_PERSONAL: ByteArray = byteArrayOf(0x00, 0x02) +private val NETWORK_KEY: ByteArray = byteArrayOf(0x10, 0x27) +private val NETWORK_NAME: ByteArray = byteArrayOf(0x10, 0x45) + +private val CRYPT_TYPE: ByteArray = byteArrayOf(0x10, 0x0F) +private val CRYPT_WEP: ByteArray = byteArrayOf(0x00, 0x02) + +/** + * This class is responsible for creating the NDEF message for the WiFi data. + */ +class WifiConfigNdefMessageBuilder @Inject constructor() { + + /** + * Creates the NDEF record for the WiFi data. + * + * @param wifiData the WiFi data to be written to the NDEF record. + * @return the NDEF record for the WiFi data. + */ + private fun createWifiRecord(wifiData: WifiData): NdefRecord { + val ssid = wifiData.ssid + val password = wifiData.password + val auth = wifiData.authType // TODO: For now, we are only supporting WPA2_PSK. + val crypt = "AES" // TODO: For now, we are only supporting AES. + val authByte = WPS_AUTH_WPA2_PERSONAL + val cryptByte = WPS_CRYPT_AES + val ssidByte = ssid.toByteArray() + val passwordByte = password.toByteArray() + val ssidLength = byteArrayOf( + floor((ssid.length / 256).toDouble()) + .toInt().toByte(), (ssid.length % 256).toByte() + ) + val passwordLength = byteArrayOf( + floor((password.length / 256).toDouble()) + .toInt().toByte(), (password.length % 256).toByte() + ) + val cred = byteArrayOf(0x00, 0x36) + val idx = byteArrayOf(0x00, 0x01, 0x01) + val mac = byteArrayOf(0x00, 0x06) // TODO: Get the mac address of the device. + val keypad = byteArrayOf(0x00, 0x0B) // TODO: Get the keypad of the device. + + val payload: ByteArray = concat( + CREDENTIAL_FIELD_ID, cred, + NETWORK_IDX, idx, + NETWORK_NAME, ssidLength, ssidByte, + AUTH_TYPE, AUTH_WPA_PERSONAL, authByte, + CRYPT_TYPE, CRYPT_WEP, cryptByte, + NETWORK_KEY, passwordLength, passwordByte + ) + return NdefRecord.createMime(NFC_TOKEN_MIME_TYPE, payload) + } + + /** + * Creates the NDEF message for the WiFi data. + * + * @param wifiNetwork the WiFi data to be written to the NDEF message. + * @return the NDEF message for the WiFi data. + */ + fun createNdefMessage(wifiNetwork: WifiData): NdefMessage { + val version = byteArrayOf(((0x1 shl 4) or (0x2)).toByte()) + // TODO: Not sure is this is needed. + val handOverRecord = NdefRecord( + NdefRecord.TNF_WELL_KNOWN, + NdefRecord.RTD_HANDOVER_REQUEST, + ByteArray(0), + version + ) + // TODO: Not sure is this is needed. + val aar = NdefRecord.createApplicationRecord(ContactsContract.Directory.PACKAGE_NAME) + val wifiRecord = createWifiRecord(wifiNetwork) + return NdefMessage(arrayOf(wifiRecord)) //, handOverRecord, aar)) + } + + /** + * Concatenates the given byte arrays into a single byte array. + * + * @param arrays the byte arrays to be concatenated. + * @return the concatenated byte array. + */ + private fun concat(vararg arrays: ByteArray): ByteArray { + // Calculate the total length of the resulting ByteArray + val totalLength = arrays.sumOf { it.size } + + // Create a new ByteArray with the calculated length + val result = ByteArray(totalLength) + + // Keep track of the current position in the result array + var currentPos = 0 + + // Copy each ByteArray into the result array + for (array in arrays) { + System.arraycopy(array, 0, result, currentPos, array.size) + currentPos += array.size + } + + return result + } + +} \ No newline at end of file diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt new file mode 100644 index 00000000..aebf0f35 --- /dev/null +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.wifi.provisioner.nfc.domain + +/** + * WifiData is a data class that holds the wifi data. + * @param ssid The ssid of the wifi network. + * @param password The password of the wifi network. + * @param authType The authentication type of the wifi network. + */ +data class WifiData( + val ssid: String, + val password: String, + val authType: String, + // TODO: Add more fields as required. +) \ No newline at end of file From 2fae178d00fba03a0627c194bd8e85090935c372 Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 21 May 2024 12:48:14 +0200 Subject: [PATCH 14/78] add wifi network manually --- feature/nfc/build.gradle.kts | 1 + .../nfc/permission/utils/PermissionUtils.kt | 14 -- .../feature/nfc/view/AddWifiManuallyDialog.kt | 147 +++++++++++++ .../feature/nfc/view/DropDownView.kt | 187 +++++++++++++++++ .../feature/nfc/view/NfcProvisioningScreen.kt | 196 +++--------------- .../feature/nfc/view/PasswordDialog.kt | 113 ++++++++++ .../feature/nfc/view/TextInputField.kt | 108 ++++++++++ .../feature/nfc/view/WifiScannerView.kt | 20 +- .../NfcManagerViewModel.kt | 2 +- .../NfcProvisioningViewEvent.kt | 13 +- .../NfcProvisioningViewModel.kt | 15 +- .../NfcProvisioningViewState.kt | 3 +- feature/nfc/src/main/res/values/strings.xml | 14 ++ .../wifi/provisioner/nfc/NfcManagerForWifi.kt | 3 + .../provisioner/nfc/domain/EncryptionMode.kt | 9 + .../wifi/provisioner/nfc/domain/WifiData.kt | 1 + 16 files changed, 634 insertions(+), 212 deletions(-) create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/AddWifiManuallyDialog.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/DropDownView.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordDialog.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/TextInputField.kt rename feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/{viemodel => viewmodel}/NfcManagerViewModel.kt (91%) rename feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/{viemodel => viewmodel}/NfcProvisioningViewEvent.kt (68%) rename feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/{viemodel => viewmodel}/NfcProvisioningViewModel.kt (84%) rename feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/{viemodel => viewmodel}/NfcProvisioningViewState.kt (94%) create mode 100644 lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt diff --git a/feature/nfc/build.gradle.kts b/feature/nfc/build.gradle.kts index 097df420..27b29f20 100644 --- a/feature/nfc/build.gradle.kts +++ b/feature/nfc/build.gradle.kts @@ -17,5 +17,6 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.compose) implementation(project(":feature:common")) api(project(":lib:nfc:provisioner")) + api(project(":lib:domain")) // TODD: Remove this once feature:common is utilized in this module. implementation(libs.nordic.permissions.nfc) } \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/PermissionUtils.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/PermissionUtils.kt index be99e69f..f26f3d26 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/PermissionUtils.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/PermissionUtils.kt @@ -18,12 +18,6 @@ internal class PermissionUtils( get() = (context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager) .isWifiEnabled - val isLocationEnabled: Boolean - get() = if (dataProvider.isMarshmallowOrAbove) { - val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - LocationManagerCompat.isLocationEnabled(lm) - } else true - val isWifiAvailable: Boolean get() = context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI) @@ -47,14 +41,6 @@ internal class PermissionUtils( val areNecessaryWifiPermissionsGranted: Boolean get() = isWifiPermissionGranted - fun markWifiPermissionRequested() { - dataProvider.wifiPermissionRequested = true - } - - fun markLocationPermissionRequested() { - dataProvider.locationPermissionRequested = true - } - fun isWifiPermissionDeniedForever(context: Context): Boolean { return dataProvider.isTiramisuOrAbove && !isWifiPermissionGranted && // Wifi permission must be denied diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/AddWifiManuallyDialog.kt new file mode 100644 index 00000000..89cc87a4 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/AddWifiManuallyDialog.kt @@ -0,0 +1,147 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material.icons.outlined.Wifi +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.nfc.domain.EncryptionMode +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData +import no.nordicsemi.kotlin.wifi.provisioner.domain.AuthModeDomain + +/** + * Composable function to show the dialog to add Wi-Fi manually. + * + * @param onCancelClick The lambda to be called when the cancel button is clicked. + * @param onConfirmClick The lambda to be called when the confirm button is clicked. + */ +@Composable +internal fun AddWifiManuallyDialog( + onCancelClick: () -> Unit, + onConfirmClick: (WifiData) -> Unit, +) { + var ssid by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var showPassword by remember { mutableStateOf(false) } + var authMode by remember { mutableStateOf("") } + var encryptionMode by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = { }, + icon = { + Icon(imageVector = Icons.Outlined.Wifi, contentDescription = null) + }, + title = { + Text( + text = stringResource(id = R.string.setup_wifi_title), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + val items = AuthModeDomain.entries.map { it.name } + // Show the authentication dropdown. + DropdownView( + items = items, + label = stringResource(id = R.string.authentication), + placeholder = stringResource(id = R.string.authentication_placeholder), + defaultSelectedItem = authMode + ) { authMode = it } + + // Show the encryption dropdown. + DropdownView( + items = EncryptionMode.entries.map { it.name }, + label = stringResource(id = R.string.encryption), + placeholder = stringResource(id = R.string.encryption_placeholder), + defaultSelectedItem = encryptionMode + ) { encryptionMode = it } + + // Show the SSID field. + TextInputField( + input = ssid, + label = stringResource(id = R.string.ssid), + placeholder = stringResource(id = R.string.ssid_placeholder), + onUpdate = { ssid = it } + ) + + // Show the password field. + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text(text = stringResource(id = R.string.password)) }, + placeholder = { Text(text = stringResource(id = R.string.password_placeholder)) }, + visualTransformation = if (showPassword) + VisualTransformation.None + else + PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { showPassword = !showPassword }) { + Icon( + imageVector = if (!showPassword) + Icons.Outlined.Visibility + else Icons.Outlined.VisibilityOff, + contentDescription = null + ) + } + } + ) + } + }, + dismissButton = { + TextButton( + onClick = { + onCancelClick() + } + ) { Text(text = stringResource(id = R.string.cancel)) } + }, + confirmButton = { + TextButton( + onClick = { + onConfirmClick( + WifiData( + ssid = ssid, + password = password, + authType = authMode, + encryptionMode = encryptionMode, + ) + ) + } + ) { Text(text = stringResource(id = R.string.confirm)) } + } + ) +} + +@Preview +@Composable +private fun OpenAddWifiManuallyDialogPreview() { + AddWifiManuallyDialog( + onCancelClick = {}, + onConfirmClick = {} + ) +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/DropDownView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/DropDownView.kt new file mode 100644 index 00000000..ee636aca --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/DropDownView.kt @@ -0,0 +1,187 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.view + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuDefaults +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import no.nordicsemi.android.common.theme.NordicTheme + +/** + * Composable function to show the dropdown view. + * + * @param items The list of items to be shown in the dropdown. + * @param label The label to be shown in the dropdown. + * @param placeholder The placeholder to be shown in the dropdown. + * @param defaultSelectedItem The default selected item in the dropdown. + * @param isError The flag to show error state. + * @param errorMessage The error message to be shown. + * @param onItemSelected The callback to be called when an item is selected. + */ +@Composable +internal inline fun DropdownView( + items: List, + label: String, + placeholder: String, + defaultSelectedItem: T? = null, + isError: Boolean = false, + errorMessage: String = "", + crossinline onItemSelected: (T) -> Unit, +) { + DropDownMenu( + items = items, + label = label, + placeholder = placeholder, + itemSelected = defaultSelectedItem, + isError = isError, + errorMessage = errorMessage, + onItemSelected = { onItemSelected(it) }, + ) +} + +// Inspired from https://proandroiddev.com/improving-the-compose-dropdownmenu-88469b1ef34 +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private inline fun DropDownMenu( + items: List, + label: String, + placeholder: String, + itemSelected: T? = null, + isError: Boolean = false, + errorMessage: String = "", + crossinline onItemSelected: (T) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + var selectedText by rememberSaveable { mutableStateOf(itemSelected) } + var selectedIndex by rememberSaveable { mutableIntStateOf(items.indexOfFirst { it == selectedText }) } + + Box(modifier = Modifier.height(IntrinsicSize.Min)) { + OutlinedTextField( + value = selectedText?.toString() ?: placeholder, + onValueChange = { }, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text(text = placeholder) }, + label = { Text(text = label) }, + isError = isError, + supportingText = { + if (isError) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + } + } + }, + ) + // Transparent clickable surface on top of OutlinedTextField + Surface( + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.extraSmall) + .clickable { expanded = true }, + color = Color.Transparent, + ) { } + } + + AnimatedVisibility(visible = expanded) { + Dialog(onDismissRequest = { expanded = false }) { + Surface(shape = RoundedCornerShape(12.dp)) { + val listState = rememberLazyListState() + if (selectedIndex > -1) { + LaunchedEffect("ScrollToSelected") { + listState.scrollToItem(index = selectedIndex) + } + } + + LazyColumn(modifier = Modifier.fillMaxWidth(), state = listState) { + itemsIndexed(items) { index, item -> + val selectedItem = index == selectedIndex + val (backgroundColor, itemColor) = if (selectedItem) { + MaterialTheme.colorScheme.primary to MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.surface to MaterialTheme.colorScheme.onSurface + } + DropdownMenuItem( + colors = MenuDefaults.itemColors(itemColor), + modifier = Modifier.background(backgroundColor), + text = { Text(item.toString()) }, + onClick = { + selectedText = item + selectedIndex = index + expanded = false + onItemSelected(item) + } + ) + + if (index < items.lastIndex) { + HorizontalDivider() + } + } + } + } + } + } +} + +@Preview +@Composable +private fun NfcDropDownViewPreview() { + NordicTheme { + DropdownView( + items = listOf("English", "Spanish", "French"), + label = "Language", + defaultSelectedItem = "English", + isError = true, + placeholder = "Select language", + errorMessage = "Error message" + ) {} + } +} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt index 7934c4a4..18aa5a6e 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt @@ -10,9 +10,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -20,18 +18,11 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Wifi import androidx.compose.material.icons.filled.WifiFind -import androidx.compose.material.icons.outlined.Visibility -import androidx.compose.material.icons.outlined.VisibilityOff -import androidx.compose.material.icons.outlined.Wifi -import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue @@ -42,8 +33,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -53,16 +42,16 @@ import no.nordicsemi.android.common.theme.view.NordicAppBar import no.nordicsemi.android.wifi.provisioner.feature.nfc.R import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireLocationForWifi import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireWifi -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.AskForPassword -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.Home -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.NfcManagerViewModel -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.NfcProvisioningViewEvent -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.NfcProvisioningViewModel -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.OnBackClickEvent -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.OnPasswordConfirmedEvent -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.OnScanClickEvent -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.Provisioning -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.Scan +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.AskForPassword +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.Home +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcManagerViewModel +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcProvisioningViewEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcProvisioningViewModel +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnBackClickEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnPasswordConfirmedEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnScanClickEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.Provisioning +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.Scan @RequiresApi(Build.VERSION_CODES.M) @OptIn(ExperimentalMaterial3Api::class) @@ -118,10 +107,10 @@ internal fun NfcProvisioningScreen() { PasswordDialog( scanResult = result, onCancelClick = { openPasswordDialog = false }, - onConfirmClick = { password, scanResult -> + onConfirmClick = { wifiData -> openPasswordDialog = false // Go to the next screen. - onEvent(OnPasswordConfirmedEvent(password, scanResult)) + onEvent(OnPasswordConfirmedEvent(wifiData)) } ) } @@ -149,89 +138,20 @@ internal fun ProvisioningView( verticalArrangement = Arrangement.Center, horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally ) { - Text(text = "Hold the NFC tag near the device to provision the WiFi network.") - } - } -} - -@Composable -internal fun PasswordDialog( - scanResult: ScanResult, - onCancelClick: () -> Unit, - onConfirmClick: (String, ScanResult) -> Unit, -) { - var password by remember { mutableStateOf("") } - AlertDialog( - onDismissRequest = { }, - icon = { - Icon(imageVector = Icons.Outlined.Wifi, contentDescription = null) - }, - title = { Text( - text = "Setup WiFi", - modifier = Modifier.fillMaxWidth(), + text = "Hold the NFC tag near the device to provision the WiFi network.", textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge - ) - }, - text = { - Column { - var ssid by remember { mutableStateOf(scanResult.SSID) } - var showPassword by remember { mutableStateOf(false) } - Text( - text = "Setup WiFi", - modifier = Modifier.fillMaxWidth(), - style = MaterialTheme.typography.bodyMedium ) - Spacer(modifier = Modifier.size(size = 16.dp)) - OutlinedTextField(value = ssid, onValueChange = { ssid = it }) - Spacer(modifier = Modifier.size(size = 8.dp)) - OutlinedTextField( - value = password, - onValueChange = { password = it }, - visualTransformation = if (showPassword) - VisualTransformation.None - else - PasswordVisualTransformation(), - trailingIcon = { - IconButton(onClick = { showPassword = !showPassword }) { - Icon( - imageVector = if (!showPassword) - Icons.Outlined.VisibilityOff - else Icons.Outlined.Visibility, - contentDescription = null - ) - } - } - ) - } - }, - dismissButton = { - TextButton( - onClick = { - onCancelClick() - } - ) { Text(text = "Cancel") } - }, - confirmButton = { - TextButton( - onClick = { - onConfirmClick( - password, - scanResult - ) - } - ) { Text(text = "Confirm") } } - ) + } } @Composable private fun NfcProvisioningHomeView( onEvent: (NfcProvisioningViewEvent) -> Unit ) { - var openAddWifiDialog by remember { mutableStateOf(false) } + var isAddWifiManuallyDialogOpen by remember { mutableStateOf(false) } Column( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.Companion @@ -242,7 +162,7 @@ private fun NfcProvisioningHomeView( modifier = Modifier .clip(RoundedCornerShape(12.dp)) .clickable { - openAddWifiDialog = true + isAddWifiManuallyDialogOpen = true } ) { @@ -263,7 +183,7 @@ private fun NfcProvisioningHomeView( modifier = Modifier .clip(CircleShape) .clickable { - openAddWifiDialog = true + isAddWifiManuallyDialogOpen = true } .padding(8.dp) ) @@ -300,81 +220,17 @@ private fun NfcProvisioningHomeView( } } - if (openAddWifiDialog) { + if (isAddWifiManuallyDialogOpen) { // Open a dialog to enter the WiFi credentials manually. - OpenAddWifiDialog( - onClick = { - openAddWifiDialog = false - } + AddWifiManuallyDialog( + onCancelClick = { + isAddWifiManuallyDialogOpen = false + }, + onConfirmClick = { + isAddWifiManuallyDialogOpen = false + onEvent(OnPasswordConfirmedEvent(it)) + }, ) } } } - -@Composable -private fun OpenAddWifiDialog( - onClick: () -> Unit, -) { - AlertDialog( - onDismissRequest = { }, - icon = { - Icon(imageVector = Icons.Outlined.Wifi, contentDescription = null) - }, - title = { - Text( - text = "Setup WiFi", - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge - ) - }, - text = { - Column { - var ssid by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - var showPassword by remember { mutableStateOf(false) } - Text( - text = "Setup WiFi", - modifier = Modifier.fillMaxWidth(), - style = MaterialTheme.typography.bodyMedium - ) - Spacer(modifier = Modifier.size(size = 16.dp)) - OutlinedTextField(value = ssid, onValueChange = { ssid = it }) - Spacer(modifier = Modifier.size(size = 8.dp)) - OutlinedTextField( - value = password, - onValueChange = { password = it }, - visualTransformation = if (showPassword) - VisualTransformation.None - else - PasswordVisualTransformation(), - trailingIcon = { - IconButton(onClick = { showPassword = !showPassword }) { - Icon( - imageVector = if (!showPassword) - Icons.Outlined.Visibility - else Icons.Outlined.VisibilityOff, - contentDescription = null - ) - } - } - ) - } - }, - dismissButton = { - TextButton( - onClick = { - onClick() - } - ) { Text(text = "Cancel") } - }, - confirmButton = { - TextButton( - onClick = { - onClick() - // TODO: Connect to the WiFi network. - } - ) { Text(text = "Confirm") } - } - ) -} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordDialog.kt new file mode 100644 index 00000000..9dbef080 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordDialog.kt @@ -0,0 +1,113 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.view + +import android.net.wifi.ScanResult +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material.icons.outlined.Wifi +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData + +/** + * Composable function to show the dialog to enter the password for the selected Wi-Fi network. + * + * @param scanResult The selected Wi-Fi network. + * @param onCancelClick The lambda to be called when the cancel button is clicked. + * @param onConfirmClick The lambda to be called when the confirm button is clicked. + */ +@Composable +internal fun PasswordDialog( + scanResult: ScanResult, + onCancelClick: () -> Unit, + onConfirmClick: (WifiData) -> Unit, +) { + var password by remember { mutableStateOf("") } + AlertDialog( + onDismissRequest = { }, + icon = { + Icon(imageVector = Icons.Outlined.Wifi, contentDescription = null) + }, + title = { + Text( + text = stringResource(id = R.string.setup_wifi_title), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + var showPassword by remember { mutableStateOf(false) } + + OutlinedTextField( + value = scanResult.SSID, + readOnly = true, + label = { Text(text = stringResource(id = R.string.ssid)) }, + onValueChange = { } + ) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + visualTransformation = if (showPassword) + VisualTransformation.None + else + PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { showPassword = !showPassword }) { + Icon( + imageVector = if (!showPassword) + Icons.Outlined.VisibilityOff + else Icons.Outlined.Visibility, + contentDescription = null + ) + } + } + ) + } + }, + dismissButton = { + TextButton( + onClick = { + onCancelClick() + } + ) { Text(text = stringResource(id = R.string.cancel)) } + }, + confirmButton = { + TextButton( + onClick = { + onConfirmClick( + WifiData( + ssid = scanResult.SSID, + password = password, + authType = "WPA2-PSK", // FIXME: use it from the scanResult. + encryptionMode = "NONE" // FIXME: use it from the scanResult. + ) + ) + } + ) { Text(text = stringResource(id = R.string.confirm)) } + } + ) +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/TextInputField.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/TextInputField.kt new file mode 100644 index 00000000..ea2da254 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/TextInputField.kt @@ -0,0 +1,108 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp + +/** + * Compose view to input text in OutlinedTextField. + */ +@Composable +internal fun TextInputField( + modifier: Modifier = Modifier, + input: String, + label: String, + hint: String = "", + placeholder: String = "", + errorMessage: String = "", + errorState: Boolean = false, + onUpdate: (String) -> Unit +) { + val textColor = MaterialTheme.colorScheme.onSurface.copy( + alpha = if (input.isEmpty()) 0.5f else LocalContentColor.current.alpha + ) + OutlinedTextField( + value = input, + onValueChange = { onUpdate(it) }, + visualTransformation = if (input.isEmpty()) + PlaceholderTransformation(placeholder) else VisualTransformation.None, + + modifier = modifier + .fillMaxWidth(), + label = { Text(text = label) }, + placeholder = { + Text( + text = placeholder, + ) + }, + supportingText = { + Column { + if (errorState) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.alpha(1f) + ) + } + } + if (hint.isNotEmpty() && !errorState) { + Text( + text = hint, + modifier = Modifier.alpha(0.38f) + ) + } + } + }, + colors = OutlinedTextFieldDefaults.colors(textColor), + isError = errorState, + ) +} + +class PlaceholderTransformation(val placeholder: String) : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + return placeholderFilter(placeholder) + } +} + +fun placeholderFilter(placeholder: String): TransformedText { + + val numberOffsetTranslator = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return 0 + } + + override fun transformedToOriginal(offset: Int): Int { + return 0 + } + } + + return TransformedText(AnnotatedString(placeholder), numberOffsetTranslator) +} + diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index 31cc88d5..57c04d59 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -2,6 +2,7 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.view import android.net.wifi.ScanResult import android.net.wifi.WifiSsid +import android.os.Build import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -24,8 +25,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import no.nordicsemi.android.wifi.provisioner.feature.nfc.R import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.getScanResultSecurity -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.NfcProvisioningViewEvent -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel.OnNetworkSelectedEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcProvisioningViewEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnNetworkSelectedEvent import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState @@ -112,10 +113,17 @@ internal fun WifiList( verticalArrangement = Arrangement.spacedBy(4.dp) ) { // Display the SSID of the access point - Text( - text = getSSid(network.wifiSsid), - style = MaterialTheme.typography.bodyLarge - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Text( + text = getSSid(network.wifiSsid), + style = MaterialTheme.typography.bodyLarge + ) + } else { + Text( + text = network.SSID, + style = MaterialTheme.typography.bodyLarge + ) + } // Display the address of the access point. Text( text = network.BSSID.uppercase(), diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcManagerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt similarity index 91% rename from feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcManagerViewModel.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt index c55f6bde..d589a85c 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcManagerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel +package no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel import android.app.Activity import android.nfc.NdefMessage diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewEvent.kt similarity index 68% rename from feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewEvent.kt index c246c471..079fa21a 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewEvent.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewEvent.kt @@ -1,6 +1,7 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel +package no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel import android.net.wifi.ScanResult +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData /** * A sealed class to represent the events that can be triggered from the UI. @@ -15,7 +16,7 @@ internal data object OnScanClickEvent : NfcProvisioningViewEvent /** * Event triggered when the back button is clicked. */ -internal data object OnBackClickEvent: NfcProvisioningViewEvent +internal data object OnBackClickEvent : NfcProvisioningViewEvent /** * Event triggered when the wifi network is selected. @@ -27,10 +28,8 @@ internal data class OnNetworkSelectedEvent(val network: ScanResult) : NfcProvisi /** * Event triggered when password is confirmed. * - * @param password The password entered by the user. - * @param network The selected network. + * @param wifiData The selected wifi network. */ internal data class OnPasswordConfirmedEvent( - val password: String, - val network: ScanResult, - ) : NfcProvisioningViewEvent + val wifiData: WifiData, +) : NfcProvisioningViewEvent diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewModel.kt similarity index 84% rename from feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewModel.kt index e6815b1e..1c17eeb4 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewModel.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel +package no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel import android.nfc.NdefMessage import android.os.Build @@ -14,7 +14,6 @@ import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.wifi.provisioner.nfc.WifiConfigNdefMessageBuilder import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading -import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData import javax.inject.Inject @RequiresApi(Build.VERSION_CODES.M) @@ -60,16 +59,8 @@ internal class NfcProvisioningViewModel @Inject constructor( is OnPasswordConfirmedEvent -> { // Create a NdefMessage with the network details. - ndefMessage = wifiConfigNdefMessageBuilder.createNdefMessage( - wifiNetwork = WifiData( - ssid = event.network.SSID, - password = event.password, - authType = "WPA2-PSK", // TODO: For now, we are only supporting WPA2_PSK. - ) - ) - /* wifiConfigNdefMessageBuilder.onNfcTap(activity = event.activity - ,ndefMessage)*/ - + ndefMessage = wifiConfigNdefMessageBuilder.createNdefMessage(event.wifiData) + // Navigate to the NFC tag screen. _viewState.value = _viewState.value.copy( view = Provisioning ) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewState.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewState.kt similarity index 94% rename from feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewState.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewState.kt index 9bd8530d..9e256769 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viemodel/NfcProvisioningViewState.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewState.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.viemodel +package no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel import android.net.wifi.ScanResult import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading @@ -35,4 +35,3 @@ internal data class AskForPassword( ) : NfcProvisioningView internal data object Provisioning : NfcProvisioningView - diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index 028bcbc7..31f28c40 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -23,4 +23,18 @@ Error occurred while scanning for networks. Unknown error occurred. + + + Setup Wi-Fi + Authentication + Select authentication + SSID + Enter SSID + Encryption + Select encryption + Password + Enter password + + Cancel + Confirm \ No newline at end of file diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt index 72ab4977..9a18585b 100644 --- a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt @@ -76,6 +76,9 @@ class NfcManagerForWifi @Inject constructor( } catch (e: Exception) { e.printStackTrace() } + } else { + // The tag does not support Ndef or NdefFormatable. + // Show an error message. } } } diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt new file mode 100644 index 00000000..99b69799 --- /dev/null +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt @@ -0,0 +1,9 @@ +package no.nordicsemi.android.wifi.provisioner.nfc.domain + +enum class EncryptionMode(val value: Int) { + NONE(0), + WEP(1), + TKIP(2), + AES(3), + AES_TKIP(4); +} diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt index aebf0f35..9eb32ea0 100644 --- a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt @@ -10,5 +10,6 @@ data class WifiData( val ssid: String, val password: String, val authType: String, + val encryptionMode: String = "NONE", // TODO: Add more fields as required. ) \ No newline at end of file From 664c11001fa0ace927bbbfa37e1dc310566ca9ce Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 21 May 2024 12:48:48 +0200 Subject: [PATCH 15/78] removed unused imports --- .../provisioner/feature/nfc/permission/utils/PermissionUtils.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/PermissionUtils.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/PermissionUtils.kt index f26f3d26..18fe2bdb 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/PermissionUtils.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/utils/PermissionUtils.kt @@ -5,10 +5,8 @@ import android.app.Activity import android.content.Context import android.content.ContextWrapper import android.content.pm.PackageManager -import android.location.LocationManager import android.net.wifi.WifiManager import androidx.core.content.ContextCompat -import androidx.core.location.LocationManagerCompat internal class PermissionUtils( private val context: Context, From c134110406386d449e3bccdde9eb207eaf771117 Mon Sep 17 00:00:00 2001 From: hiar Date: Wed, 22 May 2024 12:09:43 +0200 Subject: [PATCH 16/78] Updated Ndef message builder --- .../nfc/WifiConfigNdefMessageBuilder.kt | 149 ++++++++---------- .../nfc/domain/WifiHandoverDataType.kt | 62 ++++++++ 2 files changed, 130 insertions(+), 81 deletions(-) create mode 100644 lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiConfigNdefMessageBuilder.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiConfigNdefMessageBuilder.kt index 940df572..b5f6f921 100644 --- a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiConfigNdefMessageBuilder.kt +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiConfigNdefMessageBuilder.kt @@ -2,24 +2,19 @@ package no.nordicsemi.android.wifi.provisioner.nfc import android.nfc.NdefMessage import android.nfc.NdefRecord -import android.provider.ContactsContract import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_FIELD_ID +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_WPA2_PSK +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.CREDENTIAL_FIELD_ID +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.ENC_TYPE_AES +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.ENC_TYPE_FIELD_ID +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.MAX_MAC_ADDRESS_SIZE_BYTES +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.NETWORK_KEY_FIELD_ID +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.NFC_TOKEN_MIME_TYPE +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.SSID_FIELD_ID +import java.nio.ByteBuffer +import java.nio.charset.Charset import javax.inject.Inject -import kotlin.math.floor - -internal const val NFC_TOKEN_MIME_TYPE: String = "application/vnd.wfa.wsc" -private val CREDENTIAL_FIELD_ID = byteArrayOf(0x10, 0x0e) -private val NETWORK_IDX: ByteArray = byteArrayOf(0x10, 0x26) -private val WPS_AUTH_WPA2_PERSONAL: ByteArray = byteArrayOf(0x00, 0x20) - -private val WPS_CRYPT_AES: ByteArray = byteArrayOf(0x00, 0x08) -private val AUTH_TYPE: ByteArray = byteArrayOf(0x10, 0x03) -private val AUTH_WPA_PERSONAL: ByteArray = byteArrayOf(0x00, 0x02) -private val NETWORK_KEY: ByteArray = byteArrayOf(0x10, 0x27) -private val NETWORK_NAME: ByteArray = byteArrayOf(0x10, 0x45) - -private val CRYPT_TYPE: ByteArray = byteArrayOf(0x10, 0x0F) -private val CRYPT_WEP: ByteArray = byteArrayOf(0x00, 0x02) /** * This class is responsible for creating the NDEF message for the WiFi data. @@ -27,88 +22,80 @@ private val CRYPT_WEP: ByteArray = byteArrayOf(0x00, 0x02) class WifiConfigNdefMessageBuilder @Inject constructor() { /** - * Creates the NDEF record for the WiFi data. + * Creates the NDEF message for the WiFi data. * - * @param wifiData the WiFi data to be written to the NDEF record. - * @return the NDEF record for the WiFi data. + * @param wifiNetwork the WiFi data to be written to the NDEF message. + * @return the NDEF message for the WiFi data. */ - private fun createWifiRecord(wifiData: WifiData): NdefRecord { - val ssid = wifiData.ssid - val password = wifiData.password - val auth = wifiData.authType // TODO: For now, we are only supporting WPA2_PSK. - val crypt = "AES" // TODO: For now, we are only supporting AES. - val authByte = WPS_AUTH_WPA2_PERSONAL - val cryptByte = WPS_CRYPT_AES - val ssidByte = ssid.toByteArray() - val passwordByte = password.toByteArray() - val ssidLength = byteArrayOf( - floor((ssid.length / 256).toDouble()) - .toInt().toByte(), (ssid.length % 256).toByte() - ) - val passwordLength = byteArrayOf( - floor((password.length / 256).toDouble()) - .toInt().toByte(), (password.length % 256).toByte() - ) - val cred = byteArrayOf(0x00, 0x36) - val idx = byteArrayOf(0x00, 0x01, 0x01) - val mac = byteArrayOf(0x00, 0x06) // TODO: Get the mac address of the device. - val keypad = byteArrayOf(0x00, 0x0B) // TODO: Get the keypad of the device. - - val payload: ByteArray = concat( - CREDENTIAL_FIELD_ID, cred, - NETWORK_IDX, idx, - NETWORK_NAME, ssidLength, ssidByte, - AUTH_TYPE, AUTH_WPA_PERSONAL, authByte, - CRYPT_TYPE, CRYPT_WEP, cryptByte, - NETWORK_KEY, passwordLength, passwordByte - ) - return NdefRecord.createMime(NFC_TOKEN_MIME_TYPE, payload) + fun createNdefMessage(wifiNetwork: WifiData): NdefMessage { + return generateNdefMessage(wifiNetwork) } /** - * Creates the NDEF message for the WiFi data. + * Generates the NDEF message for the given WiFi network. * - * @param wifiNetwork the WiFi data to be written to the NDEF message. - * @return the NDEF message for the WiFi data. + * @param wifiNetwork the WiFi network to be written to the NDEF message. */ - fun createNdefMessage(wifiNetwork: WifiData): NdefMessage { - val version = byteArrayOf(((0x1 shl 4) or (0x2)).toByte()) - // TODO: Not sure is this is needed. - val handOverRecord = NdefRecord( - NdefRecord.TNF_WELL_KNOWN, - NdefRecord.RTD_HANDOVER_REQUEST, - ByteArray(0), - version + private fun generateNdefMessage(wifiNetwork: WifiData): NdefMessage { + val payload: ByteArray = generateNdefPayload(wifiNetwork) + val empty = byteArrayOf() + + val mimeRecord = NdefRecord( + NdefRecord.TNF_MIME_MEDIA, + NFC_TOKEN_MIME_TYPE.toByteArray(Charset.forName("US-ASCII")), + empty, + payload ) - // TODO: Not sure is this is needed. - val aar = NdefRecord.createApplicationRecord(ContactsContract.Directory.PACKAGE_NAME) - val wifiRecord = createWifiRecord(wifiNetwork) - return NdefMessage(arrayOf(wifiRecord)) //, handOverRecord, aar)) + + return NdefMessage(arrayOf(mimeRecord)) } /** - * Concatenates the given byte arrays into a single byte array. + * Generates the NDEF payload for the given WiFi network. * - * @param arrays the byte arrays to be concatenated. - * @return the concatenated byte array. + * @param wifiNetwork the WiFi network to be written to the NDEF message. */ - private fun concat(vararg arrays: ByteArray): ByteArray { - // Calculate the total length of the resulting ByteArray - val totalLength = arrays.sumOf { it.size } + private fun generateNdefPayload(wifiNetwork: WifiData): ByteArray { + val ssid: String = wifiNetwork.ssid + val ssidSize = ssid.toByteArray().size.toShort() + val authType: Short = AUTH_TYPE_WPA2_PSK + val networkKey: String = wifiNetwork.password + val networkKeySize = networkKey.toByteArray().size.toShort() - // Create a new ByteArray with the calculated length - val result = ByteArray(totalLength) + val macAddress = ByteArray(MAX_MAC_ADDRESS_SIZE_BYTES) + for (i in 0 until MAX_MAC_ADDRESS_SIZE_BYTES) { + macAddress[i] = 0xff.toByte() + } - // Keep track of the current position in the result array - var currentPos = 0 + /* Fill buffer */ + val bufferSize = 24 + ssidSize + networkKeySize // size of required credential attributes - // Copy each ByteArray into the result array - for (array in arrays) { - System.arraycopy(array, 0, result, currentPos, array.size) - currentPos += array.size - } + // Create a buffer with the required size + val buffer = ByteBuffer.allocate(bufferSize) + buffer.putShort(CREDENTIAL_FIELD_ID) + buffer.putShort((bufferSize - 4).toShort()) + + // Add the SSID + buffer.putShort(SSID_FIELD_ID) + buffer.putShort(ssidSize) + buffer.put(ssid.toByteArray()) + + // Add authentication type + buffer.putShort(AUTH_TYPE_FIELD_ID) + buffer.putShort(2.toShort()) + buffer.putShort(authType) + + // Add encryption type + buffer.putShort(ENC_TYPE_FIELD_ID) + buffer.putShort(2.toShort()) + buffer.putShort(ENC_TYPE_AES) + + // Add network key / password + buffer.putShort(NETWORK_KEY_FIELD_ID) + buffer.putShort(networkKeySize) + buffer.put(networkKey.toByteArray()) - return result + return buffer.array() } } \ No newline at end of file diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt new file mode 100644 index 00000000..30917607 --- /dev/null +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt @@ -0,0 +1,62 @@ +package no.nordicsemi.android.wifi.provisioner.nfc.domain + +/** + * WifiHandoverDataType is an object that holds the data types for the Wi-Fi handover data. + */ +internal object WifiHandoverDataType { + + /** + * The MIME type for the Wi-Fi Simple Configuration Token. + */ + const val NFC_TOKEN_MIME_TYPE: String = "application/vnd.wfa.wsc" + + /** + * The Credential field ID. + */ + const val CREDENTIAL_FIELD_ID: Short = 0x100e + + /** + * The Network Index field ID. + */ + const val NETWORK_INDEX_FIELD_ID: Short = 0x1026 + const val NETWORK_INDEX_DEFAULT_VALUE: Byte = 0x01.toByte() + + /** + * The SSID field ID. + */ + const val SSID_FIELD_ID: Short = 0x1045 + const val MAX_SSID_SIZE_BYTES: Int = 32 + + /** + * The Encryption Type field ID and encryption types. + */ + const val ENC_TYPE_FIELD_ID: Short = 0x100f + const val ENC_TYPE_NONE: Short = 0x0001 + const val ENC_TYPE_WEP: Short = 0x0002 // deprecated + const val ENC_TYPE_TKIP: Short = 0x0004 // deprecated -> only with mixed mode (0x000c) + const val ENC_TYPE_AES: Short = 0x0008 // includes CCMP and GCMP + const val ENC_TYPE_AES_TKIP: Short = 0x000c // mixed mode + + /** + * The Authentication Type field ID and authentication types. + */ + const val AUTH_TYPE_FIELD_ID: Short = 0x1003 + const val AUTH_TYPE_EXPECTED_SIZE: Short = 2 + const val AUTH_TYPE_OPEN: Short = 0x0001 + const val AUTH_TYPE_WPA_PSK: Short = 0x0002 + const val AUTH_TYPE_WPA_EAP: Short = 0x0008 + const val AUTH_TYPE_WPA2_EAP: Short = 0x0010 + const val AUTH_TYPE_WPA2_PSK: Short = 0x0020 + + /** + * The Network key (wifi password) field ID. + */ + const val NETWORK_KEY_FIELD_ID: Short = 0x1027 + const val MAX_NETWORK_KEY_SIZE_BYTES: Int = 64 + + /** + * The MAC Address field ID. + */ + const val MAC_ADDRESS_FIELD_ID: Short = 0x1020 + const val MAX_MAC_ADDRESS_SIZE_BYTES = 6 +} From 97cb5f974515690d38a100814c9cf3502b458c1d Mon Sep 17 00:00:00 2001 From: hiar Date: Wed, 22 May 2024 15:55:37 +0200 Subject: [PATCH 17/78] Updated password input field --- .../feature/nfc/view/AddWifiManuallyDialog.kt | 34 ++---- .../feature/nfc/view/PasswordDialog.kt | 30 ++--- .../feature/nfc/view/PasswordInputField.kt | 110 ++++++++++++++++++ .../feature/nfc/view/TextInputField.kt | 2 +- feature/nfc/src/main/res/values/strings.xml | 2 +- 5 files changed, 132 insertions(+), 46 deletions(-) create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordInputField.kt diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/AddWifiManuallyDialog.kt index 89cc87a4..b6c762a9 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/AddWifiManuallyDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/AddWifiManuallyDialog.kt @@ -4,14 +4,10 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Visibility -import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material.icons.outlined.Wifi import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -21,8 +17,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -85,31 +79,19 @@ internal fun AddWifiManuallyDialog( // Show the SSID field. TextInputField( input = ssid, - label = stringResource(id = R.string.ssid), + label = stringResource(id = R.string.ssid_label), placeholder = stringResource(id = R.string.ssid_placeholder), onUpdate = { ssid = it } ) // Show the password field. - OutlinedTextField( - value = password, - onValueChange = { password = it }, - label = { Text(text = stringResource(id = R.string.password)) }, - placeholder = { Text(text = stringResource(id = R.string.password_placeholder)) }, - visualTransformation = if (showPassword) - VisualTransformation.None - else - PasswordVisualTransformation(), - trailingIcon = { - IconButton(onClick = { showPassword = !showPassword }) { - Icon( - imageVector = if (!showPassword) - Icons.Outlined.Visibility - else Icons.Outlined.VisibilityOff, - contentDescription = null - ) - } - } + PasswordInputField( + input = password, + label = stringResource(id = R.string.password), + placeholder = stringResource(id = R.string.password_placeholder), + showPassword = showPassword, + onShowPassChange = { showPassword = !showPassword }, + onUpdate = { password = it }, ) } }, diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordDialog.kt index 9dbef080..5febde5f 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordDialog.kt @@ -11,8 +11,10 @@ import androidx.compose.material.icons.outlined.Wifi import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -62,29 +64,21 @@ internal fun PasswordDialog( ) { var showPassword by remember { mutableStateOf(false) } + // Show the SSID of the selected network. The SSID is read-only. OutlinedTextField( value = scanResult.SSID, readOnly = true, - label = { Text(text = stringResource(id = R.string.ssid)) }, + label = { Text(text = stringResource(id = R.string.ssid_label)) }, onValueChange = { } ) - OutlinedTextField( - value = password, - onValueChange = { password = it }, - visualTransformation = if (showPassword) - VisualTransformation.None - else - PasswordVisualTransformation(), - trailingIcon = { - IconButton(onClick = { showPassword = !showPassword }) { - Icon( - imageVector = if (!showPassword) - Icons.Outlined.VisibilityOff - else Icons.Outlined.Visibility, - contentDescription = null - ) - } - } + // Show the password field. + PasswordInputField( + input = password, + label = stringResource(id = R.string.password), + placeholder = stringResource(id = R.string.password_placeholder), + showPassword = showPassword, + onShowPassChange = { showPassword = !showPassword }, + onUpdate = { password = it }, ) } }, diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordInputField.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordInputField.kt new file mode 100644 index 00000000..cf762fef --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordInputField.kt @@ -0,0 +1,110 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +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.draw.alpha +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp + +/** + * Compose view to input password in OutlinedTextField. + */ +@Composable +fun PasswordInputField( + input: String, + label: String, + placeholder: String, + isError: Boolean = false, + errorMessage: String = "", + hint: String = "", + showPassword: Boolean, + onShowPassChange : (Boolean) -> Unit = {}, + onUpdate: (String) -> Unit, +) { + var isShowPassword by remember { mutableStateOf(showPassword) } + val textColor = MaterialTheme.colorScheme.onSurface.copy( + alpha = if (input.isEmpty()) 0.5f else LocalContentColor.current.alpha + ) + OutlinedTextField( + value = input, + onValueChange = { onUpdate(it) }, + visualTransformation = if (input.isEmpty()) { + PlaceholderTransformation(placeholder) + } else { + if (isShowPassword) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + } + }, + label = { Text(text = label) }, + placeholder = { + Text( + text = placeholder, + ) + }, + supportingText = { + Column { + if (isError) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.alpha(1f) + ) + } + } + if (hint.isNotEmpty() && !isError) { + Text( + text = hint, + modifier = Modifier.alpha(0.38f) + ) + } + } + }, + trailingIcon = { + IconButton(onClick = { + + isShowPassword = !isShowPassword + onShowPassChange(isShowPassword) + }) { + Icon( + imageVector = if (!isShowPassword) + Icons.Outlined.Visibility + else Icons.Outlined.VisibilityOff, + contentDescription = null + ) + } + }, + colors = OutlinedTextFieldDefaults.colors(textColor), + isError = isError, + ) +} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/TextInputField.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/TextInputField.kt index ea2da254..e6b61479 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/TextInputField.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/TextInputField.kt @@ -85,7 +85,7 @@ internal fun TextInputField( ) } -class PlaceholderTransformation(val placeholder: String) : VisualTransformation { +class PlaceholderTransformation(private val placeholder: String) : VisualTransformation { override fun filter(text: AnnotatedString): TransformedText { return placeholderFilter(placeholder) } diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index 31f28c40..1a19da7c 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -28,7 +28,7 @@ Setup Wi-Fi Authentication Select authentication - SSID + SSID Enter SSID Encryption Select encryption From 9aefd70b73c88d181e3ba56bb54c118635adb651 Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 23 May 2024 15:19:04 +0200 Subject: [PATCH 18/78] Separated home, wifi scanner, and nfc logic --- .../android/wifi/provisioner/MainActivity.kt | 2 +- build.gradle.kts | 1 + .../feature/nfc/NfcDestinations.kt | 34 ++++ .../AddWifiManuallyDialog.kt | 44 +++-- .../nfc/{view => uicomponent}/DropDownView.kt | 2 +- .../OutlinedCardItem.kt} | 62 +++--- .../{view => uicomponent}/PasswordDialog.kt | 42 ++-- .../PasswordInputField.kt | 2 +- .../nfc/{view => uicomponent}/RssiIconView.kt | 2 +- .../{view => uicomponent}/TextInputField.kt | 2 +- .../feature/nfc/view/NfcDestinations.kt | 29 --- .../feature/nfc/view/NfcProvisioningScreen.kt | 183 ++++-------------- .../provisioner/feature/nfc/view/NfcScreen.kt | 76 ++++++++ .../nfc/view/WiFiAccessPointsForNfcScreen.kt | 12 -- .../feature/nfc/view/WifiScannerView.kt | 171 +++++++++++----- .../nfc/viewmodel/NfcManagerViewModel.kt | 19 +- .../nfc/viewmodel/NfcProvisioningViewEvent.kt | 22 +-- .../nfc/viewmodel/NfcProvisioningViewModel.kt | 67 +------ .../nfc/viewmodel/NfcProvisioningViewState.kt | 37 ---- .../nfc/viewmodel/WifiScannerViewModel.kt | 116 +++++++++-- .../src/main/res/drawable/nfc_scanning.xml | 12 ++ feature/nfc/src/main/res/values/strings.xml | 4 +- lib/nfc/provisioner/build.gradle.kts | 1 + ...essageBuilder.kt => NdefMessageBuilder.kt} | 2 +- .../wifi/provisioner/nfc/domain/WifiData.kt | 6 +- 25 files changed, 511 insertions(+), 439 deletions(-) create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/NfcDestinations.kt rename feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/{view => uicomponent}/AddWifiManuallyDialog.kt (75%) rename feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/{view => uicomponent}/DropDownView.kt (99%) rename feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/{view/NfcRecordOutlinedCardItem.kt => uicomponent/OutlinedCardItem.kt} (57%) rename feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/{view => uicomponent}/PasswordDialog.kt (73%) rename feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/{view => uicomponent}/PasswordInputField.kt (98%) rename feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/{view => uicomponent}/RssiIconView.kt (97%) rename feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/{view => uicomponent}/TextInputField.kt (98%) delete mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcDestinations.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt delete mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WiFiAccessPointsForNfcScreen.kt delete mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewState.kt create mode 100644 feature/nfc/src/main/res/drawable/nfc_scanning.xml rename lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/{WifiConfigNdefMessageBuilder.kt => NdefMessageBuilder.kt} (98%) diff --git a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt index c086b2d3..38194797 100644 --- a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt +++ b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt @@ -45,7 +45,7 @@ import no.nordicsemi.android.common.navigation.NavigationView import no.nordicsemi.android.common.theme.NordicActivity import no.nordicsemi.android.common.theme.NordicTheme import no.nordicsemi.android.wifi.provisioner.ble.view.BleProvisioningDestinations -import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.NfcProvisionerDestinations +import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcProvisionerDestinations import no.nordicsemi.android.wifi.provisioner.softap.view.SoftApProvisionerDestinations @AndroidEntryPoint diff --git a/build.gradle.kts b/build.gradle.kts index 7c56c389..25067335 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -44,4 +44,5 @@ plugins { alias(libs.plugins.nordic.feature) apply false alias(libs.plugins.kotlin.android) apply false id("org.jetbrains.kotlin.jvm") version "1.9.21" apply false + alias(libs.plugins.kotlin.parcelize) apply false } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/NfcDestinations.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/NfcDestinations.kt new file mode 100644 index 00000000..bc938121 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/NfcDestinations.kt @@ -0,0 +1,34 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc + +import android.os.Build +import androidx.annotation.RequiresApi +import no.nordicsemi.android.common.navigation.createDestination +import no.nordicsemi.android.common.navigation.createSimpleDestination +import no.nordicsemi.android.common.navigation.defineDestination +import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.NfcProvisioningScreen +import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.NfcScreen +import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.WifiScannerView +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData + +val NfcProvisionerDestinationId = createSimpleDestination("nfc-provider-destination") +val WifiScannerDestinationId = + createSimpleDestination( + name = "wifi-scanner-destination", + ) + +val NfcDestinationId = + createDestination("provision-over-nfc-destination") + + +@RequiresApi(Build.VERSION_CODES.M) +val NfcProvisionerDestinations = listOf( + defineDestination(NfcProvisionerDestinationId) { + NfcProvisioningScreen() + }, + defineDestination(WifiScannerDestinationId) { + WifiScannerView() + }, + defineDestination(NfcDestinationId) { + NfcScreen() + } +) \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt similarity index 75% rename from feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/AddWifiManuallyDialog.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt index b6c762a9..2756ae37 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/AddWifiManuallyDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt @@ -1,8 +1,10 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.view +package no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Wifi import androidx.compose.material3.AlertDialog @@ -13,7 +15,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -36,11 +38,13 @@ internal fun AddWifiManuallyDialog( onCancelClick: () -> Unit, onConfirmClick: (WifiData) -> Unit, ) { - var ssid by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - var showPassword by remember { mutableStateOf(false) } - var authMode by remember { mutableStateOf("") } - var encryptionMode by remember { mutableStateOf("") } + var ssid by rememberSaveable { mutableStateOf("") } + var password by rememberSaveable { mutableStateOf("") } + var showPassword by rememberSaveable { mutableStateOf(false) } + var authMode by rememberSaveable { mutableStateOf("") } + var encryptionMode by rememberSaveable { mutableStateOf("") } + + var isSsidEmpty by rememberSaveable { mutableStateOf(false) } AlertDialog( onDismissRequest = { }, @@ -58,6 +62,7 @@ internal fun AddWifiManuallyDialog( text = { Column( verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.verticalScroll(rememberScrollState()) ) { val items = AuthModeDomain.entries.map { it.name } // Show the authentication dropdown. @@ -81,7 +86,11 @@ internal fun AddWifiManuallyDialog( input = ssid, label = stringResource(id = R.string.ssid_label), placeholder = stringResource(id = R.string.ssid_placeholder), - onUpdate = { ssid = it } + errorState = isSsidEmpty && ssid.isEmpty(), + onUpdate = { + ssid = it + isSsidEmpty = ssid.isEmpty() + } ) // Show the password field. @@ -105,14 +114,19 @@ internal fun AddWifiManuallyDialog( confirmButton = { TextButton( onClick = { - onConfirmClick( - WifiData( - ssid = ssid, - password = password, - authType = authMode, - encryptionMode = encryptionMode, + if (ssid.isEmpty()) { + isSsidEmpty = true + return@TextButton + } else { + onConfirmClick( + WifiData( + ssid = ssid, + password = password, + authType = authMode, + encryptionMode = encryptionMode, + ) ) - ) + } } ) { Text(text = stringResource(id = R.string.confirm)) } } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/DropDownView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt similarity index 99% rename from feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/DropDownView.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt index ee636aca..18a60ece 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/DropDownView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.view +package no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcRecordOutlinedCardItem.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/OutlinedCardItem.kt similarity index 57% rename from feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcRecordOutlinedCardItem.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/OutlinedCardItem.kt index 9dd600ab..efd6918a 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcRecordOutlinedCardItem.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/OutlinedCardItem.kt @@ -1,5 +1,6 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.view +package no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -7,15 +8,18 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.TipsAndUpdates import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview @@ -31,49 +35,55 @@ import no.nordicsemi.android.common.theme.nordicBlue * @param content The content of the record. */ @Composable -fun NfcRecordOutlinedCardItem( +fun OutlinedCardItem( headline: String, description: @Composable (RowScope.(TextStyle) -> Unit), icon: ImageVector, + onCardClick: () -> Unit = {}, content: @Composable (RowScope.() -> Unit), ) { - Row( - verticalAlignment = Alignment.CenterVertically, + OutlinedCard( modifier = Modifier - .padding(8.dp) - .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .clickable { onCardClick() } ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.nordicBlue, - modifier = Modifier.size(28.dp) - ) - - Column( - modifier = Modifier.padding(8.dp) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() ) { - Text( - text = headline, - style = MaterialTheme.typography.titleMedium, + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.nordicBlue, + modifier = Modifier.size(28.dp) ) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.alpha(0.7f) + Column( + modifier = Modifier.padding(8.dp) ) { - description(MaterialTheme.typography.bodySmall) + Text( + text = headline, + style = MaterialTheme.typography.titleMedium, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.alpha(0.7f) + ) { + description(MaterialTheme.typography.bodySmall) + } } + content() } - content() } - } @Preview @Composable -private fun NfcRecordOutlinedCardItemPreview() { +private fun OutlinedCardItemPreview() { NordicTheme { - NfcRecordOutlinedCardItem( + OutlinedCardItem( headline = "URI record", description = { Text( diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt similarity index 73% rename from feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordDialog.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt index 5febde5f..922f9245 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt @@ -1,31 +1,24 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.view +package no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent import android.net.wifi.ScanResult import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Visibility -import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material.icons.outlined.Wifi import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import no.nordicsemi.android.wifi.provisioner.feature.nfc.R @@ -44,7 +37,9 @@ internal fun PasswordDialog( onCancelClick: () -> Unit, onConfirmClick: (WifiData) -> Unit, ) { - var password by remember { mutableStateOf("") } + var password by rememberSaveable { mutableStateOf("") } + var isPasswordEmpty by rememberSaveable { mutableStateOf(false) } + AlertDialog( onDismissRequest = { }, icon = { @@ -62,7 +57,7 @@ internal fun PasswordDialog( Column( verticalArrangement = Arrangement.spacedBy(8.dp), ) { - var showPassword by remember { mutableStateOf(false) } + var showPassword by rememberSaveable { mutableStateOf(false) } // Show the SSID of the selected network. The SSID is read-only. OutlinedTextField( @@ -77,8 +72,13 @@ internal fun PasswordDialog( label = stringResource(id = R.string.password), placeholder = stringResource(id = R.string.password_placeholder), showPassword = showPassword, + isError = isPasswordEmpty && password.isEmpty(), + errorMessage = "Password cannot be empty.", onShowPassChange = { showPassword = !showPassword }, - onUpdate = { password = it }, + onUpdate = { + password = it + isPasswordEmpty = password.isEmpty() + }, ) } }, @@ -92,14 +92,18 @@ internal fun PasswordDialog( confirmButton = { TextButton( onClick = { - onConfirmClick( - WifiData( - ssid = scanResult.SSID, - password = password, - authType = "WPA2-PSK", // FIXME: use it from the scanResult. - encryptionMode = "NONE" // FIXME: use it from the scanResult. + if (password.trim().isEmpty()) { + isPasswordEmpty = true + } else { + onConfirmClick( + WifiData( + ssid = scanResult.SSID, + password = password, + authType = "WPA2-PSK", // FIXME: use it from the scanResult. + encryptionMode = "NONE" // FIXME: use it from the scanResult. + ) ) - ) + } } ) { Text(text = stringResource(id = R.string.confirm)) } } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordInputField.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordInputField.kt similarity index 98% rename from feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordInputField.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordInputField.kt index cf762fef..2cc52607 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/PasswordInputField.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordInputField.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.view +package no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/RssiIconView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/RssiIconView.kt similarity index 97% rename from feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/RssiIconView.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/RssiIconView.kt index d37b6eb2..946950f2 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/RssiIconView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/RssiIconView.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.view +package no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/TextInputField.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/TextInputField.kt similarity index 98% rename from feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/TextInputField.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/TextInputField.kt index e6b61479..6c8259c6 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/TextInputField.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/TextInputField.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.view +package no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcDestinations.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcDestinations.kt deleted file mode 100644 index d6dc144e..00000000 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcDestinations.kt +++ /dev/null @@ -1,29 +0,0 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.view - -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.compose.runtime.getValue -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import no.nordicsemi.android.common.navigation.createDestination -import no.nordicsemi.android.common.navigation.createSimpleDestination -import no.nordicsemi.android.common.navigation.defineDestination -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewModel -import no.nordicsemi.kotlin.wifi.provisioner.feature.common.WifiData - -val NfcProvisionerDestinationId = createSimpleDestination("nfc-provider-destination") -val WiFiAccessPointsDestinationIdForNfc = createDestination( - name = "wifi-access-points-destination2" -) - -@RequiresApi(Build.VERSION_CODES.M) -val NfcProvisionerDestinations = listOf( - defineDestination(NfcProvisionerDestinationId) { - NfcProvisioningScreen() - }, - defineDestination(WiFiAccessPointsDestinationIdForNfc) { - val viewModel = hiltViewModel() - val viewEntity by viewModel.state.collectAsStateWithLifecycle() - WiFiAccessPointsForNfcScreen(viewEntity, viewModel::onEvent) - } -) \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt index 18aa5a6e..6eb01a15 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt @@ -1,8 +1,5 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.view -import android.app.Activity -import android.net.wifi.ScanResult -import android.nfc.NdefMessage import android.os.Build import androidx.annotation.RequiresApi import androidx.compose.foundation.clickable @@ -12,7 +9,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Search @@ -21,152 +17,53 @@ import androidx.compose.material.icons.filled.WifiFind import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import no.nordicsemi.android.common.permissions.nfc.RequireNfc import no.nordicsemi.android.common.theme.view.NordicAppBar import no.nordicsemi.android.wifi.provisioner.feature.nfc.R -import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireLocationForWifi -import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireWifi -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.AskForPassword -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.Home -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcManagerViewModel +import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.AddWifiManuallyDialog +import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.OutlinedCardItem import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcProvisioningViewEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcProvisioningViewModel +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnAddWifiNetworkClickEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnBackClickEvent -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnPasswordConfirmedEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnScanClickEvent -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.Provisioning -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.Scan @RequiresApi(Build.VERSION_CODES.M) @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun NfcProvisioningScreen() { val viewModel: NfcProvisioningViewModel = hiltViewModel() - val viewState by viewModel.viewState.collectAsStateWithLifecycle() val onEvent: (NfcProvisioningViewEvent) -> Unit = { viewModel.onEvent(it) } - var openPasswordDialog by remember { mutableStateOf(false) } - var scanResult: ScanResult? = null - RequireWifi { - RequireLocationForWifi { - Column( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 56.dp) - ) { - NordicAppBar( - text = stringResource(id = R.string.wifi_provision_over_nfc_appbar), - showBackButton = true, - onNavigationButtonClick = { onEvent(OnBackClickEvent) } - ) - // Show Content. - when (val s = viewState.view) { - Home -> { - // Show the home screen. - NfcProvisioningHomeView(onEvent) - } - - is Scan -> { - // Show the scanning screen. - WifiScannerView(s.networkState, onEvent) - } - - is AskForPassword -> { - // Show the screen to ask for the password. - openPasswordDialog = true - scanResult = s.network - } - - Provisioning -> { - // TODO: Show the provisioning screen. - // Publish the NDEF message to the tag. - ProvisioningView( - ndefMessage = viewModel.ndefMessage!! - ) - } - } - if (openPasswordDialog) { - // Open a dialog to enter the WiFi credentials manually. - scanResult?.let { result -> - PasswordDialog( - scanResult = result, - onCancelClick = { openPasswordDialog = false }, - onConfirmClick = { wifiData -> - openPasswordDialog = false - // Go to the next screen. - onEvent(OnPasswordConfirmedEvent(wifiData)) - } - ) - } - } - } - } - } -} - -@Composable -internal fun ProvisioningView( - ndefMessage: NdefMessage -) { - val nfcManagerVm: NfcManagerViewModel = hiltViewModel() - val context = LocalContext.current - - RequireNfc { - DisposableEffect(key1 = nfcManagerVm) { - nfcManagerVm.onScan(context as Activity, ndefMessage) - onDispose { nfcManagerVm.onPause(context) } - } - - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally - ) { - Text( - text = "Hold the NFC tag near the device to provision the WiFi network.", - textAlign = TextAlign.Center, - - ) - } - } -} - -@Composable -private fun NfcProvisioningHomeView( - onEvent: (NfcProvisioningViewEvent) -> Unit -) { - var isAddWifiManuallyDialogOpen by remember { mutableStateOf(false) } Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.Companion - .padding(8.dp) + modifier = Modifier + .fillMaxSize() + .padding(bottom = 56.dp) ) { - // Show an option to enter the WiFi credentials manually. - OutlinedCard( - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .clickable { - isAddWifiManuallyDialogOpen = true - } - + NordicAppBar( + text = stringResource(id = R.string.wifi_provision_over_nfc_appbar), + showBackButton = true, + onNavigationButtonClick = { onEvent(OnBackClickEvent) } + ) + // Show the home screen. + var isDialogOpen by rememberSaveable { mutableStateOf(false) } + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.Companion + .padding(8.dp) ) { - NfcRecordOutlinedCardItem( + // Show an option to enter the WiFi credentials manually. + OutlinedCardItem( headline = stringResource(id = R.string.enter_wifi_credentials), description = { Text( @@ -175,6 +72,7 @@ private fun NfcProvisioningHomeView( ) }, icon = Icons.Default.Wifi, + onCardClick = { isDialogOpen = true } ) { Spacer(Modifier.weight(1f)) Icon( @@ -183,22 +81,14 @@ private fun NfcProvisioningHomeView( modifier = Modifier .clip(CircleShape) .clickable { - isAddWifiManuallyDialogOpen = true + isDialogOpen = true } .padding(8.dp) ) } - } - // Show an option to search for a WiFi network. - OutlinedCard( - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .clickable { - onEvent(OnScanClickEvent) - } - ) { - NfcRecordOutlinedCardItem( + // Show an option to search for a WiFi network. + OutlinedCardItem( headline = stringResource(id = R.string.search_for_wifi_networks), description = { Text( @@ -207,6 +97,7 @@ private fun NfcProvisioningHomeView( ) }, icon = Icons.Default.WifiFind, + onCardClick = { onEvent(OnScanClickEvent) } ) { Spacer(Modifier.weight(1f)) Icon( @@ -218,19 +109,19 @@ private fun NfcProvisioningHomeView( .padding(8.dp) ) } - } - if (isAddWifiManuallyDialogOpen) { - // Open a dialog to enter the WiFi credentials manually. - AddWifiManuallyDialog( - onCancelClick = { - isAddWifiManuallyDialogOpen = false - }, - onConfirmClick = { - isAddWifiManuallyDialogOpen = false - onEvent(OnPasswordConfirmedEvent(it)) - }, - ) + if (isDialogOpen) { + // Open a dialog to enter the WiFi credentials manually. + AddWifiManuallyDialog( + onCancelClick = { + isDialogOpen = false + }, + onConfirmClick = { + isDialogOpen = false + onEvent(OnAddWifiNetworkClickEvent(it)) + }, + ) + } } } } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt new file mode 100644 index 00000000..c05db2dc --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt @@ -0,0 +1,76 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.view + +import android.app.Activity +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import no.nordicsemi.android.common.permissions.nfc.RequireNfc +import no.nordicsemi.android.common.theme.view.NordicAppBar +import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcManagerViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun NfcScreen() { + val nfcManagerVm: NfcManagerViewModel = hiltViewModel() + val context = LocalContext.current + + Column( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 56.dp) + ) { + NordicAppBar( + text = stringResource(id = R.string.wifi_provision_over_nfc_appbar), + showBackButton = true, + onNavigationButtonClick = { nfcManagerVm.onBackNavigation() } + ) + RequireNfc { + DisposableEffect(key1 = nfcManagerVm) { + nfcManagerVm.onScan(context as Activity) + onDispose { nfcManagerVm.onPause(context) } + } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + modifier = Modifier.fillMaxSize() + ) { + Image( + painter = painterResource(id = R.drawable.nfc_scanning), + contentDescription = null, + modifier = Modifier.size(100.dp), + colorFilter = ColorFilter.tint( + MaterialTheme.colorScheme.onBackground.copy( + alpha = 0.6f + ) + ), + ) + Text( + text = "Hold the device near to the NFC tag to provision the WiFi network.", + textAlign = TextAlign.Center, + ) + Text( + text = "It might take a few seconds to connect to wifi once the NFC tag is detected.", + textAlign = TextAlign.Center, + ) + } + } + } +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WiFiAccessPointsForNfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WiFiAccessPointsForNfcScreen.kt deleted file mode 100644 index 1b5fb8f7..00000000 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WiFiAccessPointsForNfcScreen.kt +++ /dev/null @@ -1,12 +0,0 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.view - -import androidx.compose.runtime.Composable -import no.nordicsemi.kotlin.wifi.provisioner.feature.common.WifiScannerViewEntity -import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.WifiScannerViewEvent - -@Composable -internal fun WiFiAccessPointsForNfcScreen( - viewEntity: WifiScannerViewEntity, - onEvent: (WifiScannerViewEvent) -> Unit -) { -} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index 57c04d59..18cc3576 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -8,80 +8,129 @@ 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Wifi import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.common.theme.view.NordicAppBar import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.OPEN import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.getScanResultSecurity -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcProvisioningViewEvent -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnNetworkSelectedEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireLocationForWifi +import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireWifi +import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.PasswordDialog +import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.RssiIconView +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnNetworkSelectEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnPasswordCancelEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnPasswordSetEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewModel import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading -import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData /** * A composable function to display the list of available networks. - * - * @param scanningState The state of the network scanning. - * @param onEvent The event callback. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun WifiScannerView( - scanningState: NetworkState>, - onEvent: (NfcProvisioningViewEvent) -> Unit -) { +internal fun WifiScannerView() { + val wifiScannerViewModel = hiltViewModel() + val onEvent: (WifiScannerViewEvent) -> Unit = { wifiScannerViewModel.onEvent(it) } + val wifiScannerViewState by wifiScannerViewModel.viewState.collectAsStateWithLifecycle() // Show the scanning screen. - when (scanningState) { - is Error -> { - // Show the error message. - Column { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text(text = stringResource(id = R.string.error_while_scanning)) - Text(text = scanningState.t.message ?: "Unknown error occurred.") - } - } - } - - is Loading -> { - // Show the loading indicator. - Column { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - modifier = Modifier.padding(16.dp) - ) - } - } - } - - is Success -> { - // Show the list of available networks. + RequireWifi { + RequireLocationForWifi { Column( modifier = Modifier - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, + .fillMaxSize() + .padding(bottom = 56.dp) ) { - WifiList(scanningState.data, onEvent) + NordicAppBar( + text = stringResource(id = R.string.wifi_provision_over_nfc_appbar), + showBackButton = true, + onNavigationButtonClick = { } + ) + when (val scanningState = wifiScannerViewState.networks) { + is Error -> { + // Show the error message. + Column { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = stringResource(id = R.string.error_while_scanning)) + Text(text = scanningState.t.message ?: "Unknown error occurred.") + } + } + } + + is Loading -> { + // Show the loading indicator. + Column { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.padding(16.dp) + ) + } + } + } + + is Success -> { + // Show the list of available networks. + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + WifiList(scanningState.data, onEvent) + } + } + } + when (val selectedNetwork = wifiScannerViewState.selectedNetwork) { + null -> { + // Do nothing + } + + else -> { + // Show the password dialog + PasswordDialog( + scanResult = selectedNetwork, + onCancelClick = { + // Dismiss the dialog + // Set the selected network to null + onEvent(OnPasswordCancelEvent) + }) { + onEvent(OnPasswordSetEvent(it)) + } + } + } } } } @@ -96,16 +145,32 @@ internal fun WifiScannerView( @Composable internal fun WifiList( networks: List, - onEvent: (NfcProvisioningViewEvent) -> Unit + onEvent: (WifiScannerViewEvent) -> Unit ) { networks.forEach { network -> + val securityType = getScanResultSecurity(network) + val isProtected = securityType != OPEN + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .fillMaxWidth() .padding(8.dp) - .clickable { onEvent(OnNetworkSelectedEvent(network)) }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + .clickable { + if (isProtected) { + // Show the password dialog + onEvent(OnNetworkSelectEvent(network)) + } else { + // Password dialog is not required for open networks. + val wifiData = WifiData( + ssid = network.SSID, + password = "", // Todo: Verify if its empty password or null. + authType = OPEN, + ) + onEvent(OnPasswordSetEvent(wifiData)) + } + }, ) { RssiIconView(network.level) Column( @@ -132,10 +197,16 @@ internal fun WifiList( ) // Display the security type of the access point. Text( - text = getScanResultSecurity(network), + text = securityType, modifier = Modifier.alpha(0.7f) ) } + Spacer(modifier = Modifier.weight(1f)) + // TODO: Utilize the common code from no.nordicsemi.android.wifi.provisioner.ui.mapping + Icon( + imageVector = if (isProtected) Icons.Outlined.Lock else Icons.Outlined.Wifi, + contentDescription = null, + ) } HorizontalDivider() } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt index d589a85c..35049efb 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt @@ -2,8 +2,12 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel import android.app.Activity import android.nfc.NdefMessage -import androidx.lifecycle.ViewModel +import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.lifecycle.HiltViewModel +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcDestinationId +import no.nordicsemi.android.wifi.provisioner.nfc.NdefMessageBuilder import no.nordicsemi.android.wifi.provisioner.nfc.NfcManagerForWifi import javax.inject.Inject @@ -13,9 +17,14 @@ import javax.inject.Inject @HiltViewModel internal class NfcManagerViewModel @Inject constructor( private val nfcManagerForWifi: NfcManagerForWifi, -) : ViewModel() { + ndefMessageBuilder: NdefMessageBuilder, + private val navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + private val params = parameterOf(NfcDestinationId) + private val ndefMessage: NdefMessage = ndefMessageBuilder.createNdefMessage(params) - fun onScan(activity: Activity, ndefMessage: NdefMessage) { + fun onScan(activity: Activity) { nfcManagerForWifi.onNfcTap( activity = activity, message = ndefMessage @@ -25,4 +34,8 @@ internal class NfcManagerViewModel @Inject constructor( fun onPause(activity: Activity) { nfcManagerForWifi.onPause(activity) } + + fun onBackNavigation() { + navigator.navigateUp() + } } \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewEvent.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewEvent.kt index 079fa21a..f58e5d89 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewEvent.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewEvent.kt @@ -1,6 +1,5 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel -import android.net.wifi.ScanResult import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData /** @@ -14,22 +13,15 @@ internal sealed interface NfcProvisioningViewEvent internal data object OnScanClickEvent : NfcProvisioningViewEvent /** - * Event triggered when the back button is clicked. - */ -internal data object OnBackClickEvent : NfcProvisioningViewEvent - -/** - * Event triggered when the wifi network is selected. + * Event triggered when the wifi network is added manually. * - * @param network The selected network. + * @param wifiData The Wi-Fi data. */ -internal data class OnNetworkSelectedEvent(val network: ScanResult) : NfcProvisioningViewEvent +internal data class OnAddWifiNetworkClickEvent( + val wifiData: WifiData, +) : NfcProvisioningViewEvent /** - * Event triggered when password is confirmed. - * - * @param wifiData The selected wifi network. + * Event triggered when the back button is clicked. */ -internal data class OnPasswordConfirmedEvent( - val wifiData: WifiData, -) : NfcProvisioningViewEvent +internal data object OnBackClickEvent : NfcProvisioningViewEvent diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewModel.kt index 1c17eeb4..1c759ddf 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewModel.kt @@ -1,88 +1,33 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel -import android.nfc.NdefMessage import android.os.Build import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.wifi.provisioner.nfc.WifiConfigNdefMessageBuilder -import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository -import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading +import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcDestinationId +import no.nordicsemi.android.wifi.provisioner.feature.nfc.WifiScannerDestinationId import javax.inject.Inject @RequiresApi(Build.VERSION_CODES.M) @HiltViewModel internal class NfcProvisioningViewModel @Inject constructor( private val navigator: Navigator, - private val wifiManager: WifiManagerRepository, - private val wifiConfigNdefMessageBuilder: WifiConfigNdefMessageBuilder, ) : ViewModel() { - private val _viewState = MutableStateFlow(NfcProvisioningViewState()) - val viewState = _viewState.asStateFlow() - var ndefMessage: NdefMessage? = null - /** * Handles the events from the UI. */ fun onEvent(event: NfcProvisioningViewEvent) { when (event) { is OnScanClickEvent -> { - // Navigate to the Scanning screen. - _viewState.value = _viewState.value.copy( - view = Scan( - networkState = Loading(), - ) - ) - scanAvailableWifiNetworks() + navigator.navigateTo(WifiScannerDestinationId) } OnBackClickEvent -> navigator.navigateUp() - is OnNetworkSelectedEvent -> { - // Navigate to connect to the selected network. - // Ask the user to enter the password. - - // TODO: Check if the network is open or not. - // If the network is open, then connect to it directly. - // Otherwise, ask the user to enter the password. - _viewState.value = _viewState.value.copy( - view = AskForPassword( - network = event.network - ) - ) + is OnAddWifiNetworkClickEvent -> { + // Navigate to the NFC screen with the Wi-Fi data. + navigator.navigateTo(NfcDestinationId, event.wifiData) } - - is OnPasswordConfirmedEvent -> { - // Create a NdefMessage with the network details. - ndefMessage = wifiConfigNdefMessageBuilder.createNdefMessage(event.wifiData) - // Navigate to the NFC tag screen. - _viewState.value = _viewState.value.copy( - view = Provisioning - ) - } - } - } - - /** - * Scans for available Wi-Fi networks. - */ - private fun scanAvailableWifiNetworks() { - try { - wifiManager.onScan() - wifiManager.networkState.onEach { scanResults -> - _viewState.value = _viewState.value.copy( - view = Scan( - networkState = scanResults, - ) - ) - }.launchIn(viewModelScope) - } catch (e: Exception) { - e.printStackTrace() } } } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewState.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewState.kt deleted file mode 100644 index 9e256769..00000000 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewState.kt +++ /dev/null @@ -1,37 +0,0 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel - -import android.net.wifi.ScanResult -import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading -import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState - -/** - * NfcProvisioningViewState is a data class that holds the state of the NFC provisioning screen. - * @param view The current view state of the NFC provisioning screen. - */ -internal data class NfcProvisioningViewState( - val view: NfcProvisioningView = Home, -) - -/** - * NfcProvisioningView is a sealed interface that holds the different view states of the NFC provisioning screen. - */ -internal sealed interface NfcProvisioningView - -/** - * Home is a data object that represents the home screen of the NFC provisioning screen. - */ -internal data object Home : NfcProvisioningView - -/** - * Scan is a data class that represents the scanning screen of the NFC provisioning screen. - * @param networkState The network state of the scanning screen. - */ -internal data class Scan( - val networkState: NetworkState> = Loading(), -) : NfcProvisioningView - -internal data class AskForPassword( - val network: ScanResult, -) : NfcProvisioningView - -internal data object Provisioning : NfcProvisioningView diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index 4d367b0d..31795749 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -1,29 +1,111 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel +import android.net.wifi.ScanResult +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.WiFiAccessPointsDestinationIdForNfc -import no.nordicsemi.kotlin.wifi.provisioner.feature.common.WifiAggregator -import no.nordicsemi.kotlin.wifi.provisioner.feature.common.WifiData -import no.nordicsemi.kotlin.wifi.provisioner.feature.common.viewmodel.GenericWifiScannerViewModel +import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcDestinationId +import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading +import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData import javax.inject.Inject +/** + * A sealed class to represent the events that can be triggered from the UI. + */ +internal sealed interface WifiScannerViewEvent + +/** + * Event triggered when the wifi network is selected. + * + * @param network The selected network. + */ +data class OnNetworkSelectEvent( + val network: ScanResult, +) : WifiScannerViewEvent + +/** + * Event triggered when the password is confirmed. + * + * @param wifiData The Wi-Fi data. + */ +data class OnPasswordSetEvent( + val wifiData: WifiData, +) : WifiScannerViewEvent + +data object OnPasswordCancelEvent : WifiScannerViewEvent + +/** + * Event triggered when the back button is clicked. + */ +internal data object OnNavigateUpClickEvent : WifiScannerViewEvent + +/** + * A wrapper class to represent the view state of the Wi-Fi scanner screen. + */ +data class WifiScannerViewState( + val networks: NetworkState> = Loading(), + val selectedNetwork: ScanResult? = null +) + @HiltViewModel internal class WifiScannerViewModel @Inject constructor( - navigationManager: Navigator, - wifiAggregator: WifiAggregator, -) : GenericWifiScannerViewModel( - navigationManager = navigationManager, - wifiAggregator = wifiAggregator -) { - override fun navigateUp() { - navigationManager.navigateUp() + private val navigator: Navigator, + private val wifiManager: WifiManagerRepository, +) : ViewModel() { + private val _viewState = MutableStateFlow(WifiScannerViewState()) + val viewState = _viewState.asStateFlow() + + init { + scanAvailableWifiNetworks() + } + + /** + * Scans for available Wi-Fi networks. + */ + private fun scanAvailableWifiNetworks() { + try { + wifiManager.onScan() + wifiManager.networkState.onEach { scanResults -> + _viewState.value = _viewState.value.copy(networks = scanResults) + }.launchIn(viewModelScope) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun onBackClick() { + navigator.navigateUp() + } + + fun onEvent(event: WifiScannerViewEvent) { + when (event) { + is OnNetworkSelectEvent -> { + _viewState.value = _viewState.value.copy(selectedNetwork = event.network) + } + + OnNavigateUpClickEvent -> onBackClick() + is OnPasswordSetEvent -> navigateToNfcScan(event.wifiData) + OnPasswordCancelEvent -> _viewState.value = + _viewState.value.copy(selectedNetwork = null) + } } - override fun navigateUp(wifiData: WifiData) { - navigationManager.navigateUpWithResult( - from = WiFiAccessPointsDestinationIdForNfc, - result = wifiData + /** + * Navigates to the NFC scan screen. + * + * @param wifiData The Wi-Fi data. + */ + private fun navigateToNfcScan(wifiData: WifiData) { + navigator.navigateTo( + NfcDestinationId, + wifiData ) } -} \ No newline at end of file +} diff --git a/feature/nfc/src/main/res/drawable/nfc_scanning.xml b/feature/nfc/src/main/res/drawable/nfc_scanning.xml new file mode 100644 index 00000000..011e8032 --- /dev/null +++ b/feature/nfc/src/main/res/drawable/nfc_scanning.xml @@ -0,0 +1,12 @@ + + + + diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index 1a19da7c..7445e769 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -2,9 +2,9 @@ Wifi Provisioning over NFC Enter Wi-Fi credentials - Enter the Wi-Fi credentials to provision the device. + Enter the Wi-Fi credentials manually. Search for Wi-Fi networks - Search for Wi-Fi networks to provision the device. + Search for Wi-Fi networks. LOCATION PERMISSION REQUIRED diff --git a/lib/nfc/provisioner/build.gradle.kts b/lib/nfc/provisioner/build.gradle.kts index be509853..14653f6c 100644 --- a/lib/nfc/provisioner/build.gradle.kts +++ b/lib/nfc/provisioner/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.nordic.library) alias(libs.plugins.nordic.kotlin) alias(libs.plugins.nordic.hilt) + alias(libs.plugins.kotlin.parcelize) } android { diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiConfigNdefMessageBuilder.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt similarity index 98% rename from lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiConfigNdefMessageBuilder.kt rename to lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt index b5f6f921..5089ccfb 100644 --- a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiConfigNdefMessageBuilder.kt +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt @@ -19,7 +19,7 @@ import javax.inject.Inject /** * This class is responsible for creating the NDEF message for the WiFi data. */ -class WifiConfigNdefMessageBuilder @Inject constructor() { +class NdefMessageBuilder @Inject constructor() { /** * Creates the NDEF message for the WiFi data. diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt index 9eb32ea0..a37eaaea 100644 --- a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt @@ -1,15 +1,19 @@ package no.nordicsemi.android.wifi.provisioner.nfc.domain +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + /** * WifiData is a data class that holds the wifi data. * @param ssid The ssid of the wifi network. * @param password The password of the wifi network. * @param authType The authentication type of the wifi network. */ +@Parcelize data class WifiData( val ssid: String, val password: String, val authType: String, val encryptionMode: String = "NONE", // TODO: Add more fields as required. -) \ No newline at end of file +): Parcelable \ No newline at end of file From fd93c7339ecec50e01e6eb63a92b94c289aa79b0 Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 23 May 2024 16:01:15 +0200 Subject: [PATCH 19/78] Added nfc scan event --- .../provisioner/feature/nfc/view/NfcScreen.kt | 73 ++++++++++++++----- .../nfc/viewmodel/NfcManagerViewModel.kt | 1 + .../wifi/provisioner/nfc/NfcManagerForWifi.kt | 16 ++++ 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt index c05db2dc..80e6c6cf 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt @@ -3,15 +3,18 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.view import android.app.Activity import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -21,16 +24,21 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import no.nordicsemi.android.common.permissions.nfc.RequireNfc import no.nordicsemi.android.common.theme.view.NordicAppBar import no.nordicsemi.android.wifi.provisioner.feature.nfc.R import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcManagerViewModel +import no.nordicsemi.android.wifi.provisioner.nfc.Error +import no.nordicsemi.android.wifi.provisioner.nfc.Loading +import no.nordicsemi.android.wifi.provisioner.nfc.Success @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun NfcScreen() { val nfcManagerVm: NfcManagerViewModel = hiltViewModel() val context = LocalContext.current + val nfcScanEvent by nfcManagerVm.nfcScanEvent.collectAsStateWithLifecycle() Column( modifier = Modifier @@ -47,29 +55,60 @@ internal fun NfcScreen() { nfcManagerVm.onScan(context as Activity) onDispose { nfcManagerVm.onPause(context) } } + Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), modifier = Modifier.fillMaxSize() ) { - Image( - painter = painterResource(id = R.drawable.nfc_scanning), - contentDescription = null, - modifier = Modifier.size(100.dp), - colorFilter = ColorFilter.tint( - MaterialTheme.colorScheme.onBackground.copy( - alpha = 0.6f + when (val e = nfcScanEvent) { + is Error -> { + Text(text = "Nfc scan error") + Text(text = "Please try again") + Text(text = e.message) + } + + Loading -> { + // Show the loading indicator. + Column { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.padding(16.dp) + ) + } + } + } + + Success -> { + Text(text = "Nfc scan success") + Text( + text = "It might take a few seconds to connect to wifi once the NFC tag is detected.", + textAlign = TextAlign.Center, ) - ), - ) - Text( - text = "Hold the device near to the NFC tag to provision the WiFi network.", - textAlign = TextAlign.Center, - ) - Text( - text = "It might take a few seconds to connect to wifi once the NFC tag is detected.", - textAlign = TextAlign.Center, - ) + } + + null -> { + Image( + painter = painterResource(id = R.drawable.nfc_scanning), + contentDescription = null, + modifier = Modifier.size(100.dp), + colorFilter = ColorFilter.tint( + MaterialTheme.colorScheme.onBackground.copy( + alpha = 0.6f + ) + ), + ) + Text( + text = "Hold the device near to the NFC tag to provision the WiFi network.", + textAlign = TextAlign.Center, + ) + } + } + } } } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt index 35049efb..b3a1e454 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt @@ -23,6 +23,7 @@ internal class NfcManagerViewModel @Inject constructor( ) : SimpleNavigationViewModel(navigator, savedStateHandle) { private val params = parameterOf(NfcDestinationId) private val ndefMessage: NdefMessage = ndefMessageBuilder.createNdefMessage(params) + val nfcScanEvent = nfcManagerForWifi.nfcScanEvent fun onScan(activity: Activity) { nfcManagerForWifi.onNfcTap( diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt index 9a18585b..80ccb823 100644 --- a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt @@ -6,6 +6,8 @@ import android.nfc.NfcAdapter import android.nfc.Tag import android.nfc.tech.Ndef import android.nfc.tech.NdefFormatable +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject import javax.inject.Singleton @@ -26,6 +28,11 @@ private val flags = setOf( NfcFlags.NFC_BARCODE ).fold(0) { acc, flag -> acc or flag.value } +sealed interface NfcScanEvent +data object Loading : NfcScanEvent +data object Success : NfcScanEvent +data class Error(val message: String) : NfcScanEvent + /** * A class that manages the NFC adapter for the wifi provisioning. */ @@ -33,6 +40,8 @@ private val flags = setOf( class NfcManagerForWifi @Inject constructor( private val nfcAdapter: NfcAdapter?, ) { + private val _nfcScanEvent = MutableStateFlow(null) + val nfcScanEvent = _nfcScanEvent.asStateFlow() private var ndefMessage: NdefMessage? = null /** @@ -53,6 +62,7 @@ class NfcManagerForWifi @Inject constructor( * @param tag the discovered tag. */ private fun onTagDiscovered(tag: Tag?) { + _nfcScanEvent.value = Loading try { tag?.let { ndefMessage?.let { @@ -63,7 +73,9 @@ class NfcManagerForWifi @Inject constructor( ndef.connect() ndef.writeNdefMessage(it) ndef.close() + _nfcScanEvent.value = Success } catch (e: Exception) { + _nfcScanEvent.value = Error(e.message ?: "Error writing Ndef message") e.printStackTrace() } } else if (tag.techList.contains(NdefFormatable::class.java.name)) { @@ -73,16 +85,20 @@ class NfcManagerForWifi @Inject constructor( ndefFormatable.connect() ndefFormatable.format(it) ndefFormatable.close() + _nfcScanEvent.value = Success } catch (e: Exception) { + _nfcScanEvent.value = Error(e.message ?: "Error formatting Ndef message") e.printStackTrace() } } else { // The tag does not support Ndef or NdefFormatable. // Show an error message. + _nfcScanEvent.value = Error("Tag does not support Ndef or NdefFormatable") } } } } catch (e: Exception) { + _nfcScanEvent.value = Error(e.message ?: "Unknown error occurred") e.printStackTrace() } From 0e2397cdf643a5abcd6db0b6ba177dbb2d56d6ca Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 24 May 2024 12:54:58 +0200 Subject: [PATCH 20/78] Formatted Nfc publish page --- .../feature/nfc/uicomponent/NfcTextRow.kt | 48 ++++++ .../provisioner/feature/nfc/view/NfcScreen.kt | 157 ++++++++++++------ .../nfc/viewmodel/NfcManagerViewModel.kt | 4 +- .../src/main/res/drawable/nfc_scanning.xml | 12 -- feature/nfc/src/main/res/values/strings.xml | 15 ++ .../wifi/provisioner/nfc/NfcManagerForWifi.kt | 7 +- 6 files changed, 173 insertions(+), 70 deletions(-) create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/NfcTextRow.kt delete mode 100644 feature/nfc/src/main/res/drawable/nfc_scanning.xml diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/NfcTextRow.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/NfcTextRow.kt new file mode 100644 index 00000000..93c257c0 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/NfcTextRow.kt @@ -0,0 +1,48 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.common.theme.NordicTheme + +@Composable +internal fun NfcTextRow( + title: String, + text: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(4.dp), + ) { + Text( + text = title, + modifier = Modifier, + style = MaterialTheme.typography.titleMedium + ) + Text( + text = text, + modifier = Modifier.alpha(0.7f), + style = MaterialTheme.typography.bodySmall, + ) + } +} + +@Preview +@Composable +private fun NfcTextRowPreview() { + NordicTheme { + NfcTextRow( + title = "Language", + text = "en", + ) + } +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt index 80e6c6cf..df884d61 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt @@ -1,33 +1,38 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.view import android.app.Activity -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Wifi import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import no.nordicsemi.android.common.permissions.nfc.RequireNfc import no.nordicsemi.android.common.theme.view.NordicAppBar +import no.nordicsemi.android.common.theme.view.ProgressItem +import no.nordicsemi.android.common.theme.view.ProgressItemStatus +import no.nordicsemi.android.common.theme.view.WizardStepComponent +import no.nordicsemi.android.common.theme.view.WizardStepState import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.NfcTextRow import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcManagerViewModel import no.nordicsemi.android.wifi.provisioner.nfc.Error import no.nordicsemi.android.wifi.provisioner.nfc.Loading @@ -39,7 +44,13 @@ internal fun NfcScreen() { val nfcManagerVm: NfcManagerViewModel = hiltViewModel() val context = LocalContext.current val nfcScanEvent by nfcManagerVm.nfcScanEvent.collectAsStateWithLifecycle() + val ndefMessage = nfcManagerVm.ndefMessage + val wifiData = nfcManagerVm.wifiData + // Handle back navigation. + BackHandler { + nfcManagerVm.onBackNavigation() + } Column( modifier = Modifier .fillMaxSize() @@ -55,60 +66,100 @@ internal fun NfcScreen() { nfcManagerVm.onScan(context as Activity) onDispose { nfcManagerVm.onPause(context) } } - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), - modifier = Modifier.fillMaxSize() + OutlinedCard( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(8.dp) ) { - when (val e = nfcScanEvent) { - is Error -> { - Text(text = "Nfc scan error") - Text(text = "Please try again") - Text(text = e.message) + Column( + modifier = Modifier + .padding(8.dp) + ) { + // Show Ndef Record information. + WizardStepComponent( + icon = Icons.Default.Wifi, + title = stringResource(id = R.string.wifi_record), + state = WizardStepState.COMPLETED + ) { + NfcTextRow( + title = stringResource(id = R.string.ssid_title), + text = wifiData.ssid + ) + NfcTextRow( + title = stringResource(id = R.string.password_title), + text = wifiData.password + ) + NfcTextRow( + title = stringResource(id = R.string.authentication_title), + text = wifiData.authType + ) + NfcTextRow( + title = stringResource(id = R.string.encryption_title), + text = wifiData.encryptionMode + ) + NfcTextRow( + title = stringResource(id = R.string.message_size), + text = stringResource( + id = R.string.message_size_in_bytes, + ndefMessage.byteArrayLength + ) + ) } - Loading -> { - // Show the loading indicator. - Column { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - modifier = Modifier.padding(16.dp) + WizardStepComponent( + icon = Icons.Default.Edit, + title = "Discover tag", + state = WizardStepState.CURRENT, + showVerticalDivider = false, + ) { + when (val e = nfcScanEvent) { + is Error -> { + // Show the error message. + ProgressItem( + text = stringResource(id = R.string.write_failed), + status = ProgressItemStatus.ERROR + ) + Text( + text = if (e.message.length > 35) e.message.slice(0..35) else e.message, + modifier = Modifier + .alpha(0.7f) + .padding(start = 40.dp), + style = MaterialTheme.typography.bodySmall, ) } - } - } - Success -> { - Text(text = "Nfc scan success") - Text( - text = "It might take a few seconds to connect to wifi once the NFC tag is detected.", - textAlign = TextAlign.Center, - ) - } + Loading -> { + // Show the loading indicator. + ProgressItem( + text = stringResource(id = R.string.discovering_tag), + status = ProgressItemStatus.WORKING + ) + } - null -> { - Image( - painter = painterResource(id = R.drawable.nfc_scanning), - contentDescription = null, - modifier = Modifier.size(100.dp), - colorFilter = ColorFilter.tint( - MaterialTheme.colorScheme.onBackground.copy( - alpha = 0.6f + Success -> { + ProgressItem( + text = stringResource(id = R.string.write_success), + status = ProgressItemStatus.SUCCESS ) - ), - ) - Text( - text = "Hold the device near to the NFC tag to provision the WiFi network.", - textAlign = TextAlign.Center, - ) + Text( + text = stringResource(id = R.string.success_des), + modifier = Modifier + .alpha(0.7f) + .padding(start = 40.dp), + style = MaterialTheme.typography.bodySmall, + ) + } + + null -> { + ProgressItem( + text = stringResource(id = R.string.tap_nfc_tag), + status = ProgressItemStatus.WORKING + ) + } + } } } - } } } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt index b3a1e454..22961086 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt @@ -21,8 +21,8 @@ internal class NfcManagerViewModel @Inject constructor( private val navigator: Navigator, savedStateHandle: SavedStateHandle, ) : SimpleNavigationViewModel(navigator, savedStateHandle) { - private val params = parameterOf(NfcDestinationId) - private val ndefMessage: NdefMessage = ndefMessageBuilder.createNdefMessage(params) + val wifiData = parameterOf(NfcDestinationId) + val ndefMessage: NdefMessage = ndefMessageBuilder.createNdefMessage(wifiData) val nfcScanEvent = nfcManagerForWifi.nfcScanEvent fun onScan(activity: Activity) { diff --git a/feature/nfc/src/main/res/drawable/nfc_scanning.xml b/feature/nfc/src/main/res/drawable/nfc_scanning.xml deleted file mode 100644 index 011e8032..00000000 --- a/feature/nfc/src/main/res/drawable/nfc_scanning.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index 7445e769..bd886864 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -35,6 +35,21 @@ Password Enter password + + Wi-fi record + SSID + Password + Encryption + Authentication + Message size + %d bytes + Discover tag + Tap an NFC tag + Tag written successfully + It might take a few seconds to connect to the wifi network + Discovering tag… + Failed to write to the NFC tag + Cancel Confirm \ No newline at end of file diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt index 80ccb823..1be604b3 100644 --- a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt @@ -50,6 +50,7 @@ class NfcManagerForWifi @Inject constructor( * @param message the Ndef message. */ fun onNfcTap(activity: Activity, message: NdefMessage) { + _nfcScanEvent.value = null ndefMessage = message nfcAdapter?.takeIf { it.isEnabled }?.let { val readerFlag = getReaderFlag() @@ -75,7 +76,7 @@ class NfcManagerForWifi @Inject constructor( ndef.close() _nfcScanEvent.value = Success } catch (e: Exception) { - _nfcScanEvent.value = Error(e.message ?: "Error writing Ndef message") + _nfcScanEvent.value = Error(e.message ?: "Error writing NDEF message.") e.printStackTrace() } } else if (tag.techList.contains(NdefFormatable::class.java.name)) { @@ -87,13 +88,13 @@ class NfcManagerForWifi @Inject constructor( ndefFormatable.close() _nfcScanEvent.value = Success } catch (e: Exception) { - _nfcScanEvent.value = Error(e.message ?: "Error formatting Ndef message") + _nfcScanEvent.value = Error(e.message ?: "Error formatting NDEF message.") e.printStackTrace() } } else { // The tag does not support Ndef or NdefFormatable. // Show an error message. - _nfcScanEvent.value = Error("Tag does not support Ndef or NdefFormatable") + _nfcScanEvent.value = Error("Tag does not support Ndef or NdefFormatable.") } } } From f2c9cac9cd30f13696e6ab2f94ee3b143ffc60c4 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 24 May 2024 12:55:26 +0200 Subject: [PATCH 21/78] Added back handler --- .../feature/nfc/view/WifiScannerView.kt | 34 ++++++++++++------- .../nfc/viewmodel/WifiScannerViewModel.kt | 3 ++ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index 18cc3576..c01144ca 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -3,6 +3,8 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.view import android.net.wifi.ScanResult import android.net.wifi.WifiSsid import android.os.Build +import androidx.activity.compose.BackHandler +import androidx.annotation.RequiresApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -40,6 +42,7 @@ import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireLoca import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireWifi import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.PasswordDialog import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.RssiIconView +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnNavigateUpClickEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnNetworkSelectEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnPasswordCancelEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnPasswordSetEvent @@ -53,25 +56,32 @@ import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData /** * A composable function to display the list of available networks. */ +@RequiresApi(Build.VERSION_CODES.M) @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun WifiScannerView() { val wifiScannerViewModel = hiltViewModel() val onEvent: (WifiScannerViewEvent) -> Unit = { wifiScannerViewModel.onEvent(it) } val wifiScannerViewState by wifiScannerViewModel.viewState.collectAsStateWithLifecycle() + + // Handle the back press. + BackHandler { + onEvent(OnNavigateUpClickEvent) + } // Show the scanning screen. - RequireWifi { - RequireLocationForWifi { - Column( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 56.dp) - ) { - NordicAppBar( - text = stringResource(id = R.string.wifi_provision_over_nfc_appbar), - showBackButton = true, - onNavigationButtonClick = { } - ) + Column( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 56.dp) + ) { + NordicAppBar( + text = stringResource(id = R.string.wifi_provision_over_nfc_appbar), + showBackButton = true, + onNavigationButtonClick = { onEvent(OnNavigateUpClickEvent) } + ) + + RequireWifi { + RequireLocationForWifi { when (val scanningState = wifiScannerViewState.networks) { is Error -> { // Show the error message. diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index 31795749..1d52405c 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -1,6 +1,8 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel import android.net.wifi.ScanResult +import android.os.Build +import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -54,6 +56,7 @@ data class WifiScannerViewState( val selectedNetwork: ScanResult? = null ) +@RequiresApi(Build.VERSION_CODES.M) @HiltViewModel internal class WifiScannerViewModel @Inject constructor( private val navigator: Navigator, From 5befee5d5f0a707fa1c71b2c258b772b9ca01eb6 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 24 May 2024 13:03:50 +0200 Subject: [PATCH 22/78] clear dialog state before navigation --- .../feature/nfc/viewmodel/WifiScannerViewModel.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index 1d52405c..0b1cb906 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -94,7 +94,13 @@ internal class WifiScannerViewModel @Inject constructor( } OnNavigateUpClickEvent -> onBackClick() - is OnPasswordSetEvent -> navigateToNfcScan(event.wifiData) + is OnPasswordSetEvent -> { + // Close the dialog and navigate to the NFC screen. + // Needed to clear the selected network, otherwise the dialog will be shown on back press. + _viewState.value = _viewState.value.copy(selectedNetwork = null) + navigateToNfcScan(event.wifiData) + } + OnPasswordCancelEvent -> _viewState.value = _viewState.value.copy(selectedNetwork = null) } From d2574724f827a731de47ae8f8d9c2061dd1d20fc Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 24 May 2024 13:14:22 +0200 Subject: [PATCH 23/78] hide empty fields --- .../nfc/uicomponent/AddWifiManuallyDialog.kt | 1 + .../provisioner/feature/nfc/view/NfcScreen.kt | 33 +++++++++++-------- feature/nfc/src/main/res/values/strings.xml | 3 +- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt index 2756ae37..b790dad1 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt @@ -87,6 +87,7 @@ internal fun AddWifiManuallyDialog( label = stringResource(id = R.string.ssid_label), placeholder = stringResource(id = R.string.ssid_placeholder), errorState = isSsidEmpty && ssid.isEmpty(), + errorMessage = stringResource(id = R.string.ssid_error), onUpdate = { ssid = it isSsidEmpty = ssid.isEmpty() diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt index df884d61..edfed9a9 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt @@ -86,18 +86,25 @@ internal fun NfcScreen() { title = stringResource(id = R.string.ssid_title), text = wifiData.ssid ) - NfcTextRow( - title = stringResource(id = R.string.password_title), - text = wifiData.password - ) - NfcTextRow( - title = stringResource(id = R.string.authentication_title), - text = wifiData.authType - ) - NfcTextRow( - title = stringResource(id = R.string.encryption_title), - text = wifiData.encryptionMode - ) + // TODO: Change all if statements with if authType open, if yes then don't show password + if (wifiData.password.isNotEmpty()) { + NfcTextRow( + title = stringResource(id = R.string.password_title), + text = wifiData.password + ) + } + if (wifiData.authType.isNotEmpty()) { + NfcTextRow( + title = stringResource(id = R.string.authentication_title), + text = wifiData.authType + ) + } + if (wifiData.encryptionMode.isNotEmpty()) { + NfcTextRow( + title = stringResource(id = R.string.encryption_title), + text = wifiData.encryptionMode + ) + } NfcTextRow( title = stringResource(id = R.string.message_size), text = stringResource( @@ -109,7 +116,7 @@ internal fun NfcScreen() { WizardStepComponent( icon = Icons.Default.Edit, - title = "Discover tag", + title = stringResource(id = R.string.discover_tag_title), state = WizardStepState.CURRENT, showVerticalDivider = false, ) { diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index bd886864..2684bd82 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ - Wifi Provisioning over NFC + Wifi provisioning over NFC Enter Wi-Fi credentials Enter the Wi-Fi credentials manually. Search for Wi-Fi networks @@ -30,6 +30,7 @@ Select authentication SSID Enter SSID + SSID is required Encryption Select encryption Password From 17695e0a411c76d3060fdd5c6795e6313944c4b5 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 24 May 2024 15:41:51 +0200 Subject: [PATCH 24/78] Improved wifi manager --- .../provisioner/nfc/WifiManagerRepository.kt | 57 +++-------------- .../nfc/WifiManagerRepositoryImp.kt | 64 +++++++++++++++++++ .../wifi/provisioner/nfc/di/NfcAdapter.kt | 2 +- .../nfc/di/WifiManagerRepositoryModule.kt | 5 +- 4 files changed, 76 insertions(+), 52 deletions(-) create mode 100644 lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt index 2eb0573d..c677ba1e 100644 --- a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt @@ -1,62 +1,21 @@ package no.nordicsemi.android.wifi.provisioner.nfc -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter import android.net.wifi.ScanResult -import android.net.wifi.WifiManager -import android.os.Build -import androidx.annotation.RequiresApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error -import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading +import kotlinx.coroutines.flow.StateFlow import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState -import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success -@RequiresApi(Build.VERSION_CODES.M) -@SuppressWarnings("MissingPermission") -class WifiManagerRepository( - context: Context, -) { - private var wifiManager: WifiManager = - context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager - private val _networkState = MutableStateFlow>>(Loading()) - val networkState = _networkState.asStateFlow() +/** + * A repository interface to manage the wifi manager. + */ +sealed interface WifiManagerRepository { /** - * Broadcast receiver to receive the scan results. + * A state flow to represent the network state of the wifi scan. */ - private val wifiScanReceiver = object : BroadcastReceiver() { - @RequiresApi(Build.VERSION_CODES.M) - override fun onReceive(context: Context, intent: Intent) { - try { - val success = - intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false) - if (!success) { - wifiManager.startScan() - } - val results = wifiManager.scanResults - _networkState.value = Success(results) - } catch (e: Exception) { - e.printStackTrace() - _networkState.value = Error(e) - } - } - } - - init { - // Register the broadcast receiver - val intentFilter = IntentFilter() - intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) - context.registerReceiver(wifiScanReceiver, intentFilter) - } + val networkState: StateFlow>> /** * This method is used to start the wifi scan. */ - fun onScan() { - wifiManager.startScan() - } + fun onScan() } \ No newline at end of file diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt new file mode 100644 index 00000000..b2f89c36 --- /dev/null +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt @@ -0,0 +1,64 @@ +package no.nordicsemi.android.wifi.provisioner.nfc + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.wifi.ScanResult +import android.net.wifi.WifiManager +import android.os.Build +import androidx.annotation.RequiresApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading +import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success +import javax.inject.Singleton + +@RequiresApi(Build.VERSION_CODES.M) +@SuppressWarnings("MissingPermission") +@Singleton +internal class WifiManagerRepositoryImp( + context: Context, +) : WifiManagerRepository { + private var wifiManager: WifiManager = + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + private val _networkState = MutableStateFlow>>(Loading()) + override val networkState = _networkState.asStateFlow() + + /** + * Broadcast receiver to receive the scan results. + */ + private val wifiScanReceiver = object : BroadcastReceiver() { + @RequiresApi(Build.VERSION_CODES.M) + override fun onReceive(context: Context, intent: Intent) { + try { + val success = + intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false) + if (!success) { + wifiManager.startScan() + } + val results = wifiManager.scanResults + _networkState.value = Success(results) + } catch (e: Exception) { + e.printStackTrace() + _networkState.value = Error(e) + } + } + } + + init { + // Register the broadcast receiver + val intentFilter = IntentFilter() + intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) + context.registerReceiver(wifiScanReceiver, intentFilter) + } + + /** + * This method is used to start the wifi scan. + */ + override fun onScan() { + wifiManager.startScan() + } +} diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt index a0d9aa41..7f3ac218 100644 --- a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt @@ -22,7 +22,7 @@ object NfcAdapterModule { @Provides @Singleton - fun provideWifiConnectionManager(@ApplicationContext context: Context) = + fun provideNfcManagerForWifi(@ApplicationContext context: Context) = NfcManagerForWifi( nfcAdapter = provideNfcAdapter(context)!! ) diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt index ff46f535..375c0f86 100644 --- a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt +++ b/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt @@ -9,6 +9,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository +import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepositoryImp import javax.inject.Singleton @Module @@ -18,6 +19,6 @@ object WifiManagerRepositoryModule { @RequiresApi(Build.VERSION_CODES.M) @Provides @Singleton - fun provideWifiManagerRepositoryModule(@ApplicationContext context: Context) = - WifiManagerRepository(context = context) + fun provideWifiManagerRepositoryModule(@ApplicationContext context: Context): WifiManagerRepository = + WifiManagerRepositoryImp(context = context) } \ No newline at end of file From 1d37a9be9e90d19ed5e83b46d8dc6390dd562941 Mon Sep 17 00:00:00 2001 From: hiar Date: Mon, 27 May 2024 13:21:05 +0200 Subject: [PATCH 25/78] Moving nfc module to one leve up --- feature/nfc/build.gradle.kts | 2 +- lib/nfc/{provisioner => }/build.gradle.kts | 0 lib/nfc/{provisioner => }/src/main/AndroidManifest.xml | 0 .../android/wifi/provisioner/nfc/NdefMessageBuilder.kt | 0 .../android/wifi/provisioner/nfc/NfcManagerForWifi.kt | 0 .../android/wifi/provisioner/nfc/WifiManagerRepository.kt | 0 .../android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt | 0 .../no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt | 0 .../wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt | 0 .../android/wifi/provisioner/nfc/domain/EncryptionMode.kt | 0 .../android/wifi/provisioner/nfc/domain/NetworkState.kt | 0 .../nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt | 0 .../android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt | 0 13 files changed, 1 insertion(+), 1 deletion(-) rename lib/nfc/{provisioner => }/build.gradle.kts (100%) rename lib/nfc/{provisioner => }/src/main/AndroidManifest.xml (100%) rename lib/nfc/{provisioner => }/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt (100%) rename lib/nfc/{provisioner => }/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt (100%) rename lib/nfc/{provisioner => }/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt (100%) rename lib/nfc/{provisioner => }/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt (100%) rename lib/nfc/{provisioner => }/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt (100%) rename lib/nfc/{provisioner => }/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt (100%) rename lib/nfc/{provisioner => }/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt (100%) rename lib/nfc/{provisioner => }/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/NetworkState.kt (100%) rename lib/nfc/{provisioner => }/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt (100%) rename lib/nfc/{provisioner => }/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt (100%) diff --git a/feature/nfc/build.gradle.kts b/feature/nfc/build.gradle.kts index 27b29f20..72c41e55 100644 --- a/feature/nfc/build.gradle.kts +++ b/feature/nfc/build.gradle.kts @@ -16,7 +16,7 @@ dependencies { implementation(libs.androidx.compose.material.iconsExtended) implementation(libs.androidx.lifecycle.runtime.compose) implementation(project(":feature:common")) - api(project(":lib:nfc:provisioner")) + api(project(":lib:nfc")) api(project(":lib:domain")) // TODD: Remove this once feature:common is utilized in this module. implementation(libs.nordic.permissions.nfc) } \ No newline at end of file diff --git a/lib/nfc/provisioner/build.gradle.kts b/lib/nfc/build.gradle.kts similarity index 100% rename from lib/nfc/provisioner/build.gradle.kts rename to lib/nfc/build.gradle.kts diff --git a/lib/nfc/provisioner/src/main/AndroidManifest.xml b/lib/nfc/src/main/AndroidManifest.xml similarity index 100% rename from lib/nfc/provisioner/src/main/AndroidManifest.xml rename to lib/nfc/src/main/AndroidManifest.xml diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt similarity index 100% rename from lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt rename to lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt similarity index 100% rename from lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt rename to lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt similarity index 100% rename from lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt rename to lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt similarity index 100% rename from lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt rename to lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt similarity index 100% rename from lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt rename to lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt similarity index 100% rename from lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt rename to lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt similarity index 100% rename from lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt rename to lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/NetworkState.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/NetworkState.kt similarity index 100% rename from lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/NetworkState.kt rename to lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/NetworkState.kt diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt similarity index 100% rename from lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt rename to lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt diff --git a/lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt similarity index 100% rename from lib/nfc/provisioner/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt rename to lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt From 66cbc398cfafef9e2efbbc5021769f0a3d2e6d40 Mon Sep 17 00:00:00 2001 From: hiar Date: Mon, 27 May 2024 16:33:04 +0200 Subject: [PATCH 26/78] Added group by ssid filter --- .../feature/nfc/view/WifiScannerView.kt | 210 +++++++++++++----- 1 file changed, 157 insertions(+), 53 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index c01144ca..64d91b55 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -5,6 +5,7 @@ import android.net.wifi.WifiSsid import android.os.Build import androidx.activity.compose.BackHandler import androidx.annotation.RequiresApi +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -15,8 +16,14 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ExpandLess +import androidx.compose.material.icons.outlined.ExpandMore +import androidx.compose.material.icons.outlined.Group +import androidx.compose.material.icons.outlined.GroupRemove import androidx.compose.material.icons.outlined.Lock import androidx.compose.material.icons.outlined.Wifi import androidx.compose.material3.CircularProgressIndicator @@ -27,9 +34,13 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -63,6 +74,8 @@ internal fun WifiScannerView() { val wifiScannerViewModel = hiltViewModel() val onEvent: (WifiScannerViewEvent) -> Unit = { wifiScannerViewModel.onEvent(it) } val wifiScannerViewState by wifiScannerViewModel.viewState.collectAsStateWithLifecycle() + var isGroupedBySsid by rememberSaveable { mutableStateOf(false) } + val groupIcon = if (isGroupedBySsid) Icons.Outlined.GroupRemove else Icons.Outlined.Group // Handle the back press. BackHandler { @@ -75,9 +88,23 @@ internal fun WifiScannerView() { .padding(bottom = 56.dp) ) { NordicAppBar( - text = stringResource(id = R.string.wifi_provision_over_nfc_appbar), + text = stringResource(id = R.string.wifi_scanner_appbar), showBackButton = true, - onNavigationButtonClick = { onEvent(OnNavigateUpClickEvent) } + onNavigationButtonClick = { onEvent(OnNavigateUpClickEvent) }, + actions = { + // Show the group icon to group by SSID. + Icon( + imageVector = groupIcon, + contentDescription = null, + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable { + isGroupedBySsid = !isGroupedBySsid + } + .padding(8.dp) + + ) + } ) RequireWifi { @@ -119,7 +146,13 @@ internal fun WifiScannerView() { .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, ) { - WifiList(scanningState.data, onEvent) + if (isGroupedBySsid) { + // Group by SSID + GroupBySsid(scanningState.data, onEvent) + } else { + // Show the list of available networks grouped by SSIDs. + WifiList(scanningState.data, onEvent) + } } } } @@ -158,67 +191,79 @@ internal fun WifiList( onEvent: (WifiScannerViewEvent) -> Unit ) { networks.forEach { network -> - val securityType = getScanResultSecurity(network) - val isProtected = securityType != OPEN + NetworkItem( + network = network, + modifier = Modifier.padding(8.dp), + onEvent = onEvent + ) + HorizontalDivider() + } +} - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - .clickable { - if (isProtected) { - // Show the password dialog - onEvent(OnNetworkSelectEvent(network)) - } else { - // Password dialog is not required for open networks. - val wifiData = WifiData( - ssid = network.SSID, - password = "", // Todo: Verify if its empty password or null. - authType = OPEN, - ) - onEvent(OnPasswordSetEvent(wifiData)) - } - }, - ) { - RssiIconView(network.level) - Column( - modifier = Modifier.padding(8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - // Display the SSID of the access point - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Text( - text = getSSid(network.wifiSsid), - style = MaterialTheme.typography.bodyLarge - ) +@Composable +private fun NetworkItem( + network: ScanResult, + modifier: Modifier = Modifier, + onEvent: (WifiScannerViewEvent) -> Unit, +) { + val securityType = getScanResultSecurity(network) + val isProtected = securityType != OPEN + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .fillMaxWidth() + .clickable { + if (isProtected) { + // Show the password dialog + onEvent(OnNetworkSelectEvent(network)) } else { - Text( - text = network.SSID, - style = MaterialTheme.typography.bodyLarge + // Password dialog is not required for open networks. + val wifiData = WifiData( + ssid = network.SSID, + password = "", // Todo: Verify if its empty password or null. + authType = OPEN, ) + onEvent(OnPasswordSetEvent(wifiData)) } - // Display the address of the access point. + }, + ) { + RssiIconView(network.level) + Column( + modifier = Modifier.padding(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + // Display the SSID of the access point + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { Text( - text = network.BSSID.uppercase(), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.alpha(0.7f) + text = getSSid(network.wifiSsid), + style = MaterialTheme.typography.bodyLarge ) - // Display the security type of the access point. + } else { Text( - text = securityType, - modifier = Modifier.alpha(0.7f) + text = network.SSID, + style = MaterialTheme.typography.bodyLarge ) } - Spacer(modifier = Modifier.weight(1f)) - // TODO: Utilize the common code from no.nordicsemi.android.wifi.provisioner.ui.mapping - Icon( - imageVector = if (isProtected) Icons.Outlined.Lock else Icons.Outlined.Wifi, - contentDescription = null, + // Display the address of the access point. + Text( + text = network.BSSID.uppercase(), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(0.7f) + ) + // Display the security type of the access point. + Text( + text = securityType, + modifier = Modifier.alpha(0.7f) ) } - HorizontalDivider() + Spacer(modifier = Modifier.weight(1f)) + // TODO: Utilize the common code from no.nordicsemi.android.wifi.provisioner.ui.mapping + Icon( + imageVector = if (isProtected) Icons.Outlined.Lock else Icons.Outlined.Wifi, + contentDescription = null, + ) } } @@ -228,3 +273,62 @@ internal fun WifiList( fun getSSid(wifiSsid: WifiSsid?): String { return wifiSsid.toString().replace("\"", "") } + +@Composable +private fun GroupBySsid( + networks: List, + onEvent: (WifiScannerViewEvent) -> Unit, +) { + networks.groupBy { it.SSID }.forEach { (ssid, network) -> + var isExpanded by rememberSaveable { mutableStateOf(false) } + val expandIcon = if (isExpanded) Icons.Outlined.ExpandLess else Icons.Outlined.ExpandMore + + // Skip hidden networks. + if (ssid == null || ssid.isEmpty()) { + return@forEach + } + // Show the network. + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .clickable { isExpanded = !isExpanded } + .padding(8.dp) + ) { + Text(text = ssid) + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = expandIcon, + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .clickable { isExpanded = !isExpanded } + .padding(8.dp) + ) + } + // Show networks under the same SSID. + AnimatedVisibility(visible = isExpanded) { + Column { + HorizontalDivider() + network.forEach { scanResult -> + NetworkItem( + network = scanResult, + modifier = Modifier.padding( + start = 16.dp, + top = 8.dp, + end = 16.dp, + bottom = 8.dp + ), + onEvent = onEvent + ) + HorizontalDivider( + modifier = Modifier.padding(start = 16.dp), + thickness = 0.5.dp + ) + } + } + } + HorizontalDivider() + } +} From 6e2f45ed0fc2eb62aa66c4da3dbe8dc64d1784cb Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 28 May 2024 14:22:55 +0200 Subject: [PATCH 27/78] No suggestion in password field --- .../provisioner/feature/nfc/uicomponent/PasswordInputField.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordInputField.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordInputField.kt index 2cc52607..d05e3775 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordInputField.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordInputField.kt @@ -3,6 +3,7 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.outlined.Visibility @@ -22,6 +23,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp @@ -63,6 +65,7 @@ fun PasswordInputField( text = placeholder, ) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), supportingText = { Column { if (isError) { From fca460c9d86bb1fb4584b13f97d8cee0f5dff1fe Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 28 May 2024 14:23:16 +0200 Subject: [PATCH 28/78] Fixed visibility icon --- .../feature/nfc/uicomponent/PasswordInputField.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordInputField.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordInputField.kt index d05e3775..707d6c6c 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordInputField.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordInputField.kt @@ -40,13 +40,15 @@ fun PasswordInputField( errorMessage: String = "", hint: String = "", showPassword: Boolean, - onShowPassChange : (Boolean) -> Unit = {}, + onShowPassChange: (Boolean) -> Unit = {}, onUpdate: (String) -> Unit, ) { var isShowPassword by remember { mutableStateOf(showPassword) } val textColor = MaterialTheme.colorScheme.onSurface.copy( alpha = if (input.isEmpty()) 0.5f else LocalContentColor.current.alpha ) + val visibilityIcon = + if (isShowPassword) Icons.Outlined.Visibility else Icons.Outlined.VisibilityOff OutlinedTextField( value = input, onValueChange = { onUpdate(it) }, @@ -100,9 +102,7 @@ fun PasswordInputField( onShowPassChange(isShowPassword) }) { Icon( - imageVector = if (!isShowPassword) - Icons.Outlined.Visibility - else Icons.Outlined.VisibilityOff, + imageVector = visibilityIcon, contentDescription = null ) } From 5ea1a3d68f9a20487ac668a5d830244fc41e1b9b Mon Sep 17 00:00:00 2001 From: hiar Date: Wed, 29 May 2024 09:22:09 +0200 Subject: [PATCH 29/78] Fixed padding --- .../wifi/provisioner/feature/nfc/view/NfcScreen.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt index edfed9a9..eaf374f0 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt @@ -4,8 +4,8 @@ import android.app.Activity import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -68,13 +68,15 @@ internal fun NfcScreen() { } OutlinedCard( modifier = Modifier - .fillMaxWidth() .verticalScroll(rememberScrollState()) - .padding(8.dp) + .widthIn(max = 600.dp) + .padding(16.dp) + // Leave more space for the navigation bar. + .padding(bottom = 16.dp) ) { Column( modifier = Modifier - .padding(8.dp) + .padding(16.dp) ) { // Show Ndef Record information. WizardStepComponent( From a060e84acc77af25cebd23922ad2f0fe68002142 Mon Sep 17 00:00:00 2001 From: hiar Date: Wed, 29 May 2024 09:22:28 +0200 Subject: [PATCH 30/78] Fixed strings --- .../wifi/provisioner/feature/nfc/view/NfcScreen.kt | 2 +- feature/nfc/src/main/res/values/strings.xml | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt index eaf374f0..4e83de2a 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt @@ -57,7 +57,7 @@ internal fun NfcScreen() { .padding(bottom = 56.dp) ) { NordicAppBar( - text = stringResource(id = R.string.wifi_provision_over_nfc_appbar), + text = stringResource(id = R.string.ndef_publish_appbar), showBackButton = true, onNavigationButtonClick = { nfcManagerVm.onBackNavigation() } ) diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index 2684bd82..82de3da2 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -1,6 +1,13 @@ - Wifi provisioning over NFC + Provision over NFC + Wi-Fi networks + Publish + Provision over NFC + This mode allows provisioning a nRF700x device to a + Wi-Fi network by providing the Wi-Fi credentials via NFC. + + Enter Wi-Fi credentials Enter the Wi-Fi credentials manually. Search for Wi-Fi networks @@ -10,7 +17,7 @@ LOCATION PERMISSION REQUIRED Location permission is required to scan for Wi-Fi networks. - WIFI PERMISSION REQUIRED + WI-FI PERMISSION REQUIRED Wi-Fi permission is required to scan for Wi-Fi networks. WI-FI DISABLED Wi-Fi is disabled. Please enable Wi-Fi to scan for Wi-Fi networks. From 68e0fbb8c191308102fbab911dcb01544e457bb3 Mon Sep 17 00:00:00 2001 From: hiar Date: Wed, 29 May 2024 09:22:42 +0200 Subject: [PATCH 31/78] Fixed padding --- .../wifi/provisioner/feature/nfc/uicomponent/NfcTextRow.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/NfcTextRow.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/NfcTextRow.kt index 93c257c0..c589cb4b 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/NfcTextRow.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/NfcTextRow.kt @@ -20,8 +20,7 @@ internal fun NfcTextRow( ) { Column( modifier = modifier - .fillMaxWidth() - .padding(4.dp), + .fillMaxWidth(), ) { Text( text = title, From f897ac4de4cc1f66cfef9f93a491a038428606d3 Mon Sep 17 00:00:00 2001 From: hiar Date: Wed, 29 May 2024 09:23:44 +0200 Subject: [PATCH 32/78] Added sorting feature --- .../feature/nfc/view/WifiScannerView.kt | 66 ++++++++++--------- .../nfc/viewmodel/WifiScannerViewModel.kt | 22 ++++++- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index 64d91b55..dc67cdc7 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -57,12 +57,14 @@ import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnNavigateUp import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnNetworkSelectEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnPasswordCancelEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnPasswordSetEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnSortOptionSelected import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewModel import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData +import no.nordicsemi.android.wifi.provisioner.ui.view.WifiSortView /** * A composable function to display the list of available networks. @@ -73,7 +75,7 @@ import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData internal fun WifiScannerView() { val wifiScannerViewModel = hiltViewModel() val onEvent: (WifiScannerViewEvent) -> Unit = { wifiScannerViewModel.onEvent(it) } - val wifiScannerViewState by wifiScannerViewModel.viewState.collectAsStateWithLifecycle() + val viewState by wifiScannerViewModel.viewState.collectAsStateWithLifecycle() var isGroupedBySsid by rememberSaveable { mutableStateOf(false) } val groupIcon = if (isGroupedBySsid) Icons.Outlined.GroupRemove else Icons.Outlined.Group @@ -109,7 +111,7 @@ internal fun WifiScannerView() { RequireWifi { RequireLocationForWifi { - when (val scanningState = wifiScannerViewState.networks) { + when (val scanningState = viewState.networks) { is Error -> { // Show the error message. Column { @@ -141,37 +143,42 @@ internal fun WifiScannerView() { is Success -> { // Show the list of available networks. - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (isGroupedBySsid) { - // Group by SSID - GroupBySsid(scanningState.data, onEvent) - } else { - // Show the list of available networks grouped by SSIDs. - WifiList(scanningState.data, onEvent) + Column { + WifiSortView(viewState.sortOption) { + onEvent(OnSortOptionSelected(it)) + } + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (isGroupedBySsid) { + // Group by SSID + GroupBySsid(viewState.sortedItems, onEvent) + } else { + // Show the list of available networks grouped by SSIDs. + WifiList(viewState.sortedItems, onEvent) + } } } } } - when (val selectedNetwork = wifiScannerViewState.selectedNetwork) { - null -> { - // Do nothing - } + } + when (val selectedNetwork = viewState.selectedNetwork) { + null -> { + // Do nothing + } - else -> { - // Show the password dialog - PasswordDialog( - scanResult = selectedNetwork, - onCancelClick = { - // Dismiss the dialog - // Set the selected network to null - onEvent(OnPasswordCancelEvent) - }) { - onEvent(OnPasswordSetEvent(it)) - } + else -> { + // Show the password dialog + PasswordDialog( + scanResult = selectedNetwork, + onCancelClick = { + // Dismiss the dialog + // Set the selected network to null + onEvent(OnPasswordCancelEvent) + }) { + onEvent(OnPasswordSetEvent(it)) } } } @@ -222,7 +229,7 @@ private fun NetworkItem( // Password dialog is not required for open networks. val wifiData = WifiData( ssid = network.SSID, - password = "", // Todo: Verify if its empty password or null. + password = "", // Empty password for open networks authType = OPEN, ) onEvent(OnPasswordSetEvent(wifiData)) @@ -259,7 +266,6 @@ private fun NetworkItem( ) } Spacer(modifier = Modifier.weight(1f)) - // TODO: Utilize the common code from no.nordicsemi.android.wifi.provisioner.ui.mapping Icon( imageVector = if (isProtected) Icons.Outlined.Lock else Icons.Outlined.Wifi, contentDescription = null, diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index 0b1cb906..cccabb17 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -15,7 +15,9 @@ import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcDestinationId import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData +import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.WifiSortOption import javax.inject.Inject /** @@ -48,13 +50,26 @@ data object OnPasswordCancelEvent : WifiScannerViewEvent */ internal data object OnNavigateUpClickEvent : WifiScannerViewEvent +internal data class OnSortOptionSelected(val sortOption: WifiSortOption) : WifiScannerViewEvent + /** * A wrapper class to represent the view state of the Wi-Fi scanner screen. + * + * @param networks The state of the available Wi-Fi networks. + * @param selectedNetwork The selected Wi-Fi network. + * @param sortOption The selected sort option. */ data class WifiScannerViewState( val networks: NetworkState> = Loading(), - val selectedNetwork: ScanResult? = null -) + val selectedNetwork: ScanResult? = null, + val sortOption: WifiSortOption = WifiSortOption.RSSI, +) { + private val items = (networks as? Success)?.data ?: emptyList() + val sortedItems: List = when (sortOption) { + WifiSortOption.NAME -> items.sortedBy { it.SSID } + WifiSortOption.RSSI -> items.sortedByDescending { it.level } + } +} @RequiresApi(Build.VERSION_CODES.M) @HiltViewModel @@ -103,6 +118,9 @@ internal class WifiScannerViewModel @Inject constructor( OnPasswordCancelEvent -> _viewState.value = _viewState.value.copy(selectedNetwork = null) + + is OnSortOptionSelected -> _viewState.value = + _viewState.value.copy(sortOption = event.sortOption) } } From 66a0eead42ea9e3c554372528279f258eec85016 Mon Sep 17 00:00:00 2001 From: hiar Date: Wed, 29 May 2024 13:09:17 +0200 Subject: [PATCH 33/78] Fixed authmode --- .../feature/nfc/data/WifiAuthType.kt | 27 +++---- .../feature/nfc/view/WifiScannerView.kt | 6 +- .../wifi/provisioner/nfc/domain/AuthMode.kt | 74 +++++++++++++++++++ 3 files changed, 89 insertions(+), 18 deletions(-) create mode 100644 lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/AuthMode.kt diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt index faa1726e..fb65a415 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt @@ -1,26 +1,23 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.data import android.net.wifi.ScanResult - -// Constants used for different security types -const val WPA2 = "WPA2" -const val WPA = "WPA" -const val WEP = "WEP" -const val OPEN = "Open" -const val IEEE8021X = "IEEE8021X" -const val WPA_EAP = "WPA-EAP" +import android.os.Build +import no.nordicsemi.android.wifi.provisioner.nfc.domain.AuthMode /** * @return The security of a given [ScanResult]. */ fun getScanResultSecurity(scanResult: ScanResult): String { - val cap = scanResult.capabilities - val securityModes = arrayOf(WEP, WPA, WPA2, WPA_EAP, IEEE8021X) - for (i in securityModes.indices.reversed()) { - if (cap.contains(securityModes[i])) { - return securityModes[i] + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return AuthMode.getMatchedAuthMode(scanResult.securityTypes) + } else { + val cap = scanResult.capabilities + val securityModes = AuthMode.getListOfAuthModes() + for (i in securityModes.indices.reversed()) { + if (cap.contains(securityModes[i])) { + return securityModes[i] + } } + return AuthMode.OPEN.name } - - return OPEN } \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index dc67cdc7..2908f28d 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -47,7 +47,6 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import no.nordicsemi.android.common.theme.view.NordicAppBar import no.nordicsemi.android.wifi.provisioner.feature.nfc.R -import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.OPEN import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.getScanResultSecurity import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireLocationForWifi import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireWifi @@ -60,6 +59,7 @@ import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnPasswordSe import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnSortOptionSelected import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewModel +import no.nordicsemi.android.wifi.provisioner.nfc.domain.AuthMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success @@ -214,7 +214,7 @@ private fun NetworkItem( onEvent: (WifiScannerViewEvent) -> Unit, ) { val securityType = getScanResultSecurity(network) - val isProtected = securityType != OPEN + val isProtected = securityType != AuthMode.OPEN.name Row( verticalAlignment = Alignment.CenterVertically, @@ -230,7 +230,7 @@ private fun NetworkItem( val wifiData = WifiData( ssid = network.SSID, password = "", // Empty password for open networks - authType = OPEN, + authType = AuthMode.OPEN.name, ) onEvent(OnPasswordSetEvent(wifiData)) } diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/AuthMode.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/AuthMode.kt new file mode 100644 index 00000000..fb74c469 --- /dev/null +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/AuthMode.kt @@ -0,0 +1,74 @@ +package no.nordicsemi.android.wifi.provisioner.nfc.domain + +import android.util.Log + +/** + * Enum class that represents the authentication mode of a wifi network. + */ +enum class AuthMode(val id: Int) { + UNKNOWN(-1), + OPEN(0), + WEP(1), + WPA_PSK(2), + WPA2_EAP(3), + SAE(4), + EAP_WPA3_ENTERPRISE_192_BIT(5), + OWE(6), + WAPI_PSK(7), + WAPI_CERT(8), + EAP_WPA3_ENTERPRISE(9), + OSEN(10), + PASSPOINT_R1_R2(11), + PASSPOINT_R3(12), + DPP(13); // Security type for Easy Connect (DPP) network + + companion object { + /** + * List of all the authentication modes. + */ + fun getListOfAuthModes(): List { + val list = AuthMode.entries.map { mapToUi(it) } + Log.d("AAA", "getListOfAuthModes: $list") + return AuthMode.entries.map { mapToUi(it) } + } + + /** + * Get the authentication mode of a given security type. + * @param securityTypes The security type of the wifi network. + * @return The authentication mode of the wifi network. + */ + fun getMatchedAuthMode(securityTypes: IntArray): String { + securityTypes.forEach { type -> + val authMode = AuthMode.entries.find { it.id == type } + if (authMode != null) { + return mapToUi(authMode) + } + } + val authMode = AuthMode.entries.find { it.id in securityTypes } ?: OPEN + return mapToUi(authMode) + } + + /** + * Maps the authentication mode to a user friendly string. + */ + private fun mapToUi(authMode: AuthMode): String { + return when (authMode) { + UNKNOWN -> "Unknown" + OPEN -> "OPEN" + WEP -> "WEP" + WPA_PSK -> "WPA2-PSK" + WPA2_EAP -> "WPA2-EAP" + SAE -> "SAE" + EAP_WPA3_ENTERPRISE_192_BIT -> "EAP-WPA3-ENTERPRISE-192-BIT" + OWE -> "OWE" + WAPI_PSK -> "WAPI-PSK" + WAPI_CERT -> "WAPI-CERT" + EAP_WPA3_ENTERPRISE -> "EAP-WPA3-ENTERPRISE" + OSEN -> "OSEN" + PASSPOINT_R1_R2 -> "PASSPOINT-R1-R2" + PASSPOINT_R3 -> "PASSPOINT-R3" + DPP -> "DPP" + } + } + } +} From c1c6c0806aaf3344ed48853f52773af4de873ec2 Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 30 May 2024 13:19:40 +0200 Subject: [PATCH 34/78] Fixed authmode while publishing --- .../feature/nfc/data/WifiAuthType.kt | 133 ++++++++++++++++-- .../nfc/uicomponent/AddWifiManuallyDialog.kt | 4 +- .../feature/nfc/uicomponent/PasswordDialog.kt | 4 +- .../feature/nfc/view/WifiScannerView.kt | 6 +- .../provisioner/nfc/NdefMessageBuilder.kt | 35 ++++- .../wifi/provisioner/nfc/domain/AuthMode.kt | 74 ---------- .../wifi/provisioner/nfc/domain/WifiData.kt | 2 +- .../nfc/domain/WifiHandoverDataType.kt | 10 ++ 8 files changed, 173 insertions(+), 95 deletions(-) delete mode 100644 lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/AuthMode.kt diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt index fb65a415..11082a61 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt @@ -2,22 +2,129 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.data import android.net.wifi.ScanResult import android.os.Build -import no.nordicsemi.android.wifi.provisioner.nfc.domain.AuthMode + +object WifiAuthType { + /** + * Constants for security types. + * + * Note: This is for Build version sdk S and below. + */ + private const val OPEN = "Open" + private const val WEP = "WEP" + private const val WPA_PSK = "WPA-PSK" + private const val WPA2_PSK = "WPA2-PSK" + private const val WPA_WPA2_PSK = "WPA/WPA2-PSK" + private const val WPA2_EAP = "WPA2-EAP" + + private val securityTypes = listOf(OPEN, WEP, WPA_PSK, WPA2_PSK, WPA_WPA2_PSK, WPA2_EAP) + + /** + * @return The security of a given [ScanResult]. + */ + fun getSecurityTypes(scanResult: ScanResult): String { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return AuthMode.getMatchedAuthMode(scanResult.securityTypes) + } else { + val cap = scanResult.capabilities + val securityModes = securityTypes + for (i in securityModes.indices.reversed()) { + if (cap.contains(securityModes[i])) { + return mapToUi(securityModes[i]) + } + } + return mapToUi(AuthMode.OPEN.name) + } + } + + /** + * Maps the security type to a user friendly string. + * + * Note: This is for Build version sdk S and below. + */ + private fun mapToUi(mode: String): String { + return when (mode) { + "WPA-PSK", "WPA_PSK" -> "WPA-Personal" + "WPA2-PSK", "WPA2_PSK" -> "WPA2-Personal" + "WPA/WPA2-PSK", "WPA_WPA2_PSK" -> "WPA/WPA2-Personal" + "WPA2-EAP", "WPA2_EAP" -> "WPA2-Enterprise" + "WPA3-PSK", "WPA3_PSK" -> "WPA3-Personal" + "WEP" -> "Shared" + else -> "Open" + } + } + + /** + * @return The list of security types. + */ + fun authList(): List { + return securityTypes.map { mapToUi(it) } + } +} /** - * @return The security of a given [ScanResult]. + * Enum class that represents the authentication mode of a wifi network. + * + * Note: This is for Build version sdk Tiramisu (API 31) and above. */ -fun getScanResultSecurity(scanResult: ScanResult): String { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return AuthMode.getMatchedAuthMode(scanResult.securityTypes) - } else { - val cap = scanResult.capabilities - val securityModes = AuthMode.getListOfAuthModes() - for (i in securityModes.indices.reversed()) { - if (cap.contains(securityModes[i])) { - return securityModes[i] +enum class AuthMode(val id: Int) { + UNKNOWN(-1), + OPEN(0), + WEP(1), + WPA_PSK(2), + WPA2_EAP(3), + WPA3_PSK(4), // WPA3-Personal (SAE) password) + EAP_WPA3_ENTERPRISE_192_BIT(5), + OWE(6), // Opportunistic Wireless Encryption + WAPI_PSK(7), // WAPI pre-shared key (PSK) + WAPI_CERT(8), // WAPI certificate to be specified. + EAP_WPA3_ENTERPRISE(9), + OSEN(10), // Hotspot 2. + PASSPOINT_R1_R2(11), + PASSPOINT_R3(12), + DPP(13); // Security type for Easy Connect (DPP) network + + companion object { + private val stringMappings = mapOf( + UNKNOWN to "Unknown", + OPEN to "Open", + WEP to "WEP", + WPA_PSK to "WPA-Personal", + WPA2_EAP to "WPA2-Enterprise", + WPA3_PSK to "WPA3-Personal", + EAP_WPA3_ENTERPRISE_192_BIT to "EAP-WPA3-ENTERPRISE-192-BIT", + OWE to "OWE", + WAPI_PSK to "WAPI-Personal", + WAPI_CERT to "WAPI-CERT", + EAP_WPA3_ENTERPRISE to "EAP-WPA3-ENTERPRISE", + OSEN to "OSEN", + PASSPOINT_R1_R2 to "PASSPOINT-R1-R2", + PASSPOINT_R3 to "PASSPOINT-R3", + DPP to "DPP" + ) + + /** + * Get the authentication mode of a given security type. + * @param securityTypes The security type of the wifi network. + * @return The authentication mode of the wifi network. + */ + fun getMatchedAuthMode(securityTypes: IntArray): String { + val matchedType = mutableListOf() + securityTypes.forEach { type -> + val authMode = AuthMode.entries.find { it.id == type } + if (authMode != null) { + matchedType.add(authModeTOString(authMode)) + } } + return matchedType.joinToString(", ") + } + + /** + * Maps the authentication mode to a user friendly string. + * + * Note: This is for Build version sdk Tiramisu (API 31) and above. + */ + private fun authModeTOString(authMode: AuthMode): String { + return stringMappings[authMode] ?: "Unknown" } - return AuthMode.OPEN.name } -} \ No newline at end of file +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt index b790dad1..d373c6c1 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt @@ -23,9 +23,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.WifiAuthType import no.nordicsemi.android.wifi.provisioner.nfc.domain.EncryptionMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData -import no.nordicsemi.kotlin.wifi.provisioner.domain.AuthModeDomain /** * Composable function to show the dialog to add Wi-Fi manually. @@ -64,7 +64,7 @@ internal fun AddWifiManuallyDialog( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.verticalScroll(rememberScrollState()) ) { - val items = AuthModeDomain.entries.map { it.name } + val items = WifiAuthType.authList() // Show the authentication dropdown. DropdownView( items = items, diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt index 922f9245..b2c661bd 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.WifiAuthType import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData /** @@ -39,6 +40,7 @@ internal fun PasswordDialog( ) { var password by rememberSaveable { mutableStateOf("") } var isPasswordEmpty by rememberSaveable { mutableStateOf(false) } + val authMode = WifiAuthType.getSecurityTypes(scanResult) AlertDialog( onDismissRequest = { }, @@ -99,7 +101,7 @@ internal fun PasswordDialog( WifiData( ssid = scanResult.SSID, password = password, - authType = "WPA2-PSK", // FIXME: use it from the scanResult. + authType = authMode, encryptionMode = "NONE" // FIXME: use it from the scanResult. ) ) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index 2908f28d..d52c93bb 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -47,7 +47,8 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import no.nordicsemi.android.common.theme.view.NordicAppBar import no.nordicsemi.android.wifi.provisioner.feature.nfc.R -import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.getScanResultSecurity +import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.AuthMode +import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.WifiAuthType import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireLocationForWifi import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireWifi import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.PasswordDialog @@ -59,7 +60,6 @@ import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnPasswordSe import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnSortOptionSelected import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewModel -import no.nordicsemi.android.wifi.provisioner.nfc.domain.AuthMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success @@ -213,7 +213,7 @@ private fun NetworkItem( modifier: Modifier = Modifier, onEvent: (WifiScannerViewEvent) -> Unit, ) { - val securityType = getScanResultSecurity(network) + val securityType = WifiAuthType.getSecurityTypes(network) val isProtected = securityType != AuthMode.OPEN.name Row( diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt index 5089ccfb..ddb3a0b5 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt @@ -4,7 +4,13 @@ import android.nfc.NdefMessage import android.nfc.NdefRecord import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_FIELD_ID +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_OPEN +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_SHARED +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_WPA2_EAP import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_WPA2_PSK +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_WPA_EAP +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_WPA_PSK +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_WPA_WPA2_PSK import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.CREDENTIAL_FIELD_ID import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.ENC_TYPE_AES import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.ENC_TYPE_FIELD_ID @@ -12,6 +18,12 @@ import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.MA import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.NETWORK_KEY_FIELD_ID import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.NFC_TOKEN_MIME_TYPE import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.SSID_FIELD_ID +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WEP +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WPA2_ENTERPRISE +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WPA2_PSK +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WPA_EAP +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WPA_PSK +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WPA_WPA2_PSK import java.nio.ByteBuffer import java.nio.charset.Charset import javax.inject.Inject @@ -58,7 +70,7 @@ class NdefMessageBuilder @Inject constructor() { private fun generateNdefPayload(wifiNetwork: WifiData): ByteArray { val ssid: String = wifiNetwork.ssid val ssidSize = ssid.toByteArray().size.toShort() - val authType: Short = AUTH_TYPE_WPA2_PSK + val authType: Short = getAuthBytes(wifiNetwork.authType) val networkKey: String = wifiNetwork.password val networkKeySize = networkKey.toByteArray().size.toShort() @@ -98,4 +110,25 @@ class NdefMessageBuilder @Inject constructor() { return buffer.array() } + /** + * Returns the authentication type in bytes. + * + * @param auth the authentication type. + */ + private fun getAuthBytes(auth: String): Short { + // If Empty, default to WPA2-Personal + if (auth.isEmpty()) { + return AUTH_TYPE_WPA2_PSK + } + + return when { + auth.equals(WPA_PSK, ignoreCase = true) -> AUTH_TYPE_WPA_PSK + auth.equals(WPA_EAP, ignoreCase = true) -> AUTH_TYPE_WPA_EAP + auth.equals(WPA2_ENTERPRISE, ignoreCase = true) -> AUTH_TYPE_WPA2_EAP + auth.equals(WPA2_PSK, ignoreCase = true) -> AUTH_TYPE_WPA2_PSK + auth.equals(WPA_WPA2_PSK, ignoreCase = true) -> AUTH_TYPE_WPA_WPA2_PSK + auth.equals(WEP, ignoreCase = true) -> AUTH_TYPE_SHARED + else -> AUTH_TYPE_OPEN + } + } } \ No newline at end of file diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/AuthMode.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/AuthMode.kt deleted file mode 100644 index fb74c469..00000000 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/AuthMode.kt +++ /dev/null @@ -1,74 +0,0 @@ -package no.nordicsemi.android.wifi.provisioner.nfc.domain - -import android.util.Log - -/** - * Enum class that represents the authentication mode of a wifi network. - */ -enum class AuthMode(val id: Int) { - UNKNOWN(-1), - OPEN(0), - WEP(1), - WPA_PSK(2), - WPA2_EAP(3), - SAE(4), - EAP_WPA3_ENTERPRISE_192_BIT(5), - OWE(6), - WAPI_PSK(7), - WAPI_CERT(8), - EAP_WPA3_ENTERPRISE(9), - OSEN(10), - PASSPOINT_R1_R2(11), - PASSPOINT_R3(12), - DPP(13); // Security type for Easy Connect (DPP) network - - companion object { - /** - * List of all the authentication modes. - */ - fun getListOfAuthModes(): List { - val list = AuthMode.entries.map { mapToUi(it) } - Log.d("AAA", "getListOfAuthModes: $list") - return AuthMode.entries.map { mapToUi(it) } - } - - /** - * Get the authentication mode of a given security type. - * @param securityTypes The security type of the wifi network. - * @return The authentication mode of the wifi network. - */ - fun getMatchedAuthMode(securityTypes: IntArray): String { - securityTypes.forEach { type -> - val authMode = AuthMode.entries.find { it.id == type } - if (authMode != null) { - return mapToUi(authMode) - } - } - val authMode = AuthMode.entries.find { it.id in securityTypes } ?: OPEN - return mapToUi(authMode) - } - - /** - * Maps the authentication mode to a user friendly string. - */ - private fun mapToUi(authMode: AuthMode): String { - return when (authMode) { - UNKNOWN -> "Unknown" - OPEN -> "OPEN" - WEP -> "WEP" - WPA_PSK -> "WPA2-PSK" - WPA2_EAP -> "WPA2-EAP" - SAE -> "SAE" - EAP_WPA3_ENTERPRISE_192_BIT -> "EAP-WPA3-ENTERPRISE-192-BIT" - OWE -> "OWE" - WAPI_PSK -> "WAPI-PSK" - WAPI_CERT -> "WAPI-CERT" - EAP_WPA3_ENTERPRISE -> "EAP-WPA3-ENTERPRISE" - OSEN -> "OSEN" - PASSPOINT_R1_R2 -> "PASSPOINT-R1-R2" - PASSPOINT_R3 -> "PASSPOINT-R3" - DPP -> "DPP" - } - } - } -} diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt index a37eaaea..fb183cae 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt @@ -13,7 +13,7 @@ import kotlinx.parcelize.Parcelize data class WifiData( val ssid: String, val password: String, - val authType: String, + val authType: String = "", val encryptionMode: String = "NONE", // TODO: Add more fields as required. ): Parcelable \ No newline at end of file diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt index 30917607..a7d18753 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt @@ -47,6 +47,8 @@ internal object WifiHandoverDataType { const val AUTH_TYPE_WPA_EAP: Short = 0x0008 const val AUTH_TYPE_WPA2_EAP: Short = 0x0010 const val AUTH_TYPE_WPA2_PSK: Short = 0x0020 + const val AUTH_TYPE_WPA_WPA2_PSK: Short = 0x0022 + const val AUTH_TYPE_SHARED: Short = 0x0004 // deprecated "WEP" type /** * The Network key (wifi password) field ID. @@ -59,4 +61,12 @@ internal object WifiHandoverDataType { */ const val MAC_ADDRESS_FIELD_ID: Short = 0x1020 const val MAX_MAC_ADDRESS_SIZE_BYTES = 6 + + // Constants for the authentication types. + const val WPA_PSK = "WPA-Personal" + const val WPA_EAP = "WPA-Enterprise" + const val WPA2_PSK = "WPA2-Personal" + const val WPA2_ENTERPRISE = "WPA2-Enterprise" + const val WPA_WPA2_PSK = "WPA/WPA2-Personal" + const val WEP = "Shared" } From f49a9510d1554df98814185b5f6977e6d49a9d37 Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 30 May 2024 13:40:23 +0200 Subject: [PATCH 35/78] Fixed for multiple authmode --- .../wifi/provisioner/nfc/NdefMessageBuilder.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt index ddb3a0b5..79635ba5 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt @@ -122,12 +122,12 @@ class NdefMessageBuilder @Inject constructor() { } return when { - auth.equals(WPA_PSK, ignoreCase = true) -> AUTH_TYPE_WPA_PSK - auth.equals(WPA_EAP, ignoreCase = true) -> AUTH_TYPE_WPA_EAP - auth.equals(WPA2_ENTERPRISE, ignoreCase = true) -> AUTH_TYPE_WPA2_EAP - auth.equals(WPA2_PSK, ignoreCase = true) -> AUTH_TYPE_WPA2_PSK - auth.equals(WPA_WPA2_PSK, ignoreCase = true) -> AUTH_TYPE_WPA_WPA2_PSK - auth.equals(WEP, ignoreCase = true) -> AUTH_TYPE_SHARED + auth.startsWith(WPA_PSK, ignoreCase = true) -> AUTH_TYPE_WPA_PSK + auth.startsWith(WPA_EAP, ignoreCase = true) -> AUTH_TYPE_WPA_EAP + auth.startsWith(WPA2_ENTERPRISE, ignoreCase = true) -> AUTH_TYPE_WPA2_EAP + auth.startsWith(WPA2_PSK, ignoreCase = true) -> AUTH_TYPE_WPA2_PSK + auth.startsWith(WPA_WPA2_PSK, ignoreCase = true) -> AUTH_TYPE_WPA_WPA2_PSK + auth.startsWith(WEP, ignoreCase = true) -> AUTH_TYPE_SHARED else -> AUTH_TYPE_OPEN } } From 3c1f547444ef6b011297ac28a6ab4dddbffaf6f4 Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 30 May 2024 16:12:48 +0200 Subject: [PATCH 36/78] Fixed encryption mode --- .../feature/nfc/data/WifiAuthType.kt | 3 +- .../nfc/uicomponent/AddWifiManuallyDialog.kt | 2 +- .../feature/nfc/uicomponent/PasswordDialog.kt | 4 +- .../provisioner/nfc/NdefMessageBuilder.kt | 29 ++++++++- .../provisioner/nfc/domain/EncryptionMode.kt | 61 +++++++++++++++++-- .../wifi/provisioner/nfc/domain/WifiData.kt | 6 +- 6 files changed, 92 insertions(+), 13 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt index 11082a61..79888802 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt @@ -15,8 +15,9 @@ object WifiAuthType { private const val WPA2_PSK = "WPA2-PSK" private const val WPA_WPA2_PSK = "WPA/WPA2-PSK" private const val WPA2_EAP = "WPA2-EAP" + private const val WPA3_PSK = "WPA3-PSK" - private val securityTypes = listOf(OPEN, WEP, WPA_PSK, WPA2_PSK, WPA_WPA2_PSK, WPA2_EAP) + private val securityTypes = listOf(OPEN, WEP, WPA_PSK, WPA2_PSK, WPA_WPA2_PSK, WPA2_EAP, WPA3_PSK) /** * @return The security of a given [ScanResult]. diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt index d373c6c1..fe84e448 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt @@ -75,7 +75,7 @@ internal fun AddWifiManuallyDialog( // Show the encryption dropdown. DropdownView( - items = EncryptionMode.entries.map { it.name }, + items = EncryptionMode.getEncryptionList(), label = stringResource(id = R.string.encryption), placeholder = stringResource(id = R.string.encryption_placeholder), defaultSelectedItem = encryptionMode diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt index b2c661bd..16e1dfc9 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import no.nordicsemi.android.wifi.provisioner.feature.nfc.R import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.WifiAuthType +import no.nordicsemi.android.wifi.provisioner.nfc.domain.EncryptionMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData /** @@ -41,6 +42,7 @@ internal fun PasswordDialog( var password by rememberSaveable { mutableStateOf("") } var isPasswordEmpty by rememberSaveable { mutableStateOf(false) } val authMode = WifiAuthType.getSecurityTypes(scanResult) + val encryptionMode = EncryptionMode.getEncryption(scanResult) AlertDialog( onDismissRequest = { }, @@ -102,7 +104,7 @@ internal fun PasswordDialog( ssid = scanResult.SSID, password = password, authType = authMode, - encryptionMode = "NONE" // FIXME: use it from the scanResult. + encryptionMode = encryptionMode // FIXME: use it from the scanResult. ) ) } diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt index 79635ba5..4e8df1e2 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt @@ -2,6 +2,7 @@ package no.nordicsemi.android.wifi.provisioner.nfc import android.nfc.NdefMessage import android.nfc.NdefRecord +import no.nordicsemi.android.wifi.provisioner.nfc.domain.EncryptionMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_FIELD_ID import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_OPEN @@ -13,7 +14,11 @@ import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AU import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_WPA_WPA2_PSK import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.CREDENTIAL_FIELD_ID import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.ENC_TYPE_AES +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.ENC_TYPE_AES_TKIP import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.ENC_TYPE_FIELD_ID +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.ENC_TYPE_NONE +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.ENC_TYPE_TKIP +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.ENC_TYPE_WEP import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.MAX_MAC_ADDRESS_SIZE_BYTES import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.NETWORK_KEY_FIELD_ID import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.NFC_TOKEN_MIME_TYPE @@ -74,6 +79,8 @@ class NdefMessageBuilder @Inject constructor() { val networkKey: String = wifiNetwork.password val networkKeySize = networkKey.toByteArray().size.toShort() + val encType = getEncByte(wifiNetwork.encryptionMode) + val macAddress = ByteArray(MAX_MAC_ADDRESS_SIZE_BYTES) for (i in 0 until MAX_MAC_ADDRESS_SIZE_BYTES) { macAddress[i] = 0xff.toByte() @@ -100,7 +107,7 @@ class NdefMessageBuilder @Inject constructor() { // Add encryption type buffer.putShort(ENC_TYPE_FIELD_ID) buffer.putShort(2.toShort()) - buffer.putShort(ENC_TYPE_AES) + buffer.putShort(encType) // Add network key / password buffer.putShort(NETWORK_KEY_FIELD_ID) @@ -110,6 +117,26 @@ class NdefMessageBuilder @Inject constructor() { return buffer.array() } + /** + * Returns the encryption type in bytes. + * + * @param enc the encryption type. + */ + private fun getEncByte(enc: String): Short { + + // If Empty, default to AES + if (enc.isEmpty()) { + return ENC_TYPE_AES + } + return when (enc) { + EncryptionMode.WEP.name -> ENC_TYPE_WEP + EncryptionMode.TKIP.name -> ENC_TYPE_TKIP + EncryptionMode.AES.name -> ENC_TYPE_AES + EncryptionMode.AES_TKIP.name, "AES/TKIP" -> ENC_TYPE_AES_TKIP + else -> ENC_TYPE_NONE + } + } + /** * Returns the authentication type in bytes. * diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt index 99b69799..1eafc444 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt @@ -1,9 +1,58 @@ package no.nordicsemi.android.wifi.provisioner.nfc.domain -enum class EncryptionMode(val value: Int) { - NONE(0), - WEP(1), - TKIP(2), - AES(3), - AES_TKIP(4); +import android.net.wifi.ScanResult + +/** + * This enum class represents the encryption mode of a Wi-Fi network. + */ +enum class EncryptionMode { + NONE, + WEP, + TKIP, + AES, + AES_TKIP; + + companion object { + + /** + * Returns the encryption mode of the given scan result. + * + * @param result the scan result. + * @return the encryption mode. + */ + fun getEncryption(result: ScanResult): String { + val firstCapabilities = + result.capabilities.substring(1, result.capabilities.indexOf("]")) + val capabilities = firstCapabilities.split("-".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val auth = capabilities[0] + "-" + capabilities[1] + val encryptionMode = when { + auth.contains("WPA2-PSK") -> AES + auth.contains("WPA-PSK") -> TKIP + auth.contains("WPA2-EAP") -> AES + auth.contains("WPA-EAP") -> AES_TKIP // TODO: Verify this. + auth.contains("WPA/WPA2-PSK") -> AES_TKIP + auth.contains("WEP") -> WEP + auth.contains("Open") -> NONE + auth.contains("WPA3-PSK") -> AES + else -> NONE + } + + return encryptionMode.name + } + + // Encryption modes to be displayed in the UI + private val encryptionModesToUi = mapOf( + "NONE" to NONE, + "WEP" to WEP, + "TKIP" to TKIP, + "AES" to AES, + "AES/TKIP" to AES_TKIP + ) + + /** Returns the list of encryption modes to be displayed in the UI. */ + fun getEncryptionList(): List { + return encryptionModesToUi.keys.toList() + } + } } diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt index fb183cae..0ba5e4d4 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt @@ -8,12 +8,12 @@ import kotlinx.parcelize.Parcelize * @param ssid The ssid of the wifi network. * @param password The password of the wifi network. * @param authType The authentication type of the wifi network. + * @param encryptionMode The encryption mode of the wifi network. */ @Parcelize data class WifiData( val ssid: String, val password: String, val authType: String = "", - val encryptionMode: String = "NONE", - // TODO: Add more fields as required. -): Parcelable \ No newline at end of file + val encryptionMode: String = "", +) : Parcelable \ No newline at end of file From 4837c29891dd0bec398f5f738f9359c4c9dbb9c7 Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 30 May 2024 16:24:09 +0200 Subject: [PATCH 37/78] Hide password with asterisks --- .../feature/nfc/uicomponent/NfcTextRow.kt | 24 +++++++++++++++++-- .../provisioner/feature/nfc/view/NfcScreen.kt | 6 ++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/NfcTextRow.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/NfcTextRow.kt index c589cb4b..4d0e582d 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/NfcTextRow.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/NfcTextRow.kt @@ -2,14 +2,12 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import no.nordicsemi.android.common.theme.NordicTheme @Composable @@ -45,3 +43,25 @@ private fun NfcTextRowPreview() { ) } } + +@Composable +internal fun NfcPasswordRow( + title: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth(), + ) { + Text( + text = title, + modifier = Modifier, + style = MaterialTheme.typography.titleMedium + ) + Text( + text = "********", // Hide the password with asterisks. + modifier = Modifier.alpha(0.7f), + style = MaterialTheme.typography.bodySmall, + ) + } +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt index 4e83de2a..ef7313df 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt @@ -32,6 +32,7 @@ import no.nordicsemi.android.common.theme.view.ProgressItemStatus import no.nordicsemi.android.common.theme.view.WizardStepComponent import no.nordicsemi.android.common.theme.view.WizardStepState import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.NfcPasswordRow import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.NfcTextRow import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcManagerViewModel import no.nordicsemi.android.wifi.provisioner.nfc.Error @@ -90,10 +91,7 @@ internal fun NfcScreen() { ) // TODO: Change all if statements with if authType open, if yes then don't show password if (wifiData.password.isNotEmpty()) { - NfcTextRow( - title = stringResource(id = R.string.password_title), - text = wifiData.password - ) + NfcPasswordRow(title = stringResource(id = R.string.password_title)) } if (wifiData.authType.isNotEmpty()) { NfcTextRow( From c88d10ed22e9fc51547639ad29d5b76ecdde7155 Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 30 May 2024 16:28:55 +0200 Subject: [PATCH 38/78] Removed todos --- .../wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt | 2 +- .../android/wifi/provisioner/feature/nfc/view/NfcScreen.kt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt index 16e1dfc9..543a7887 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt @@ -104,7 +104,7 @@ internal fun PasswordDialog( ssid = scanResult.SSID, password = password, authType = authMode, - encryptionMode = encryptionMode // FIXME: use it from the scanResult. + encryptionMode = encryptionMode ) ) } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt index ef7313df..708f44f8 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt @@ -89,7 +89,6 @@ internal fun NfcScreen() { title = stringResource(id = R.string.ssid_title), text = wifiData.ssid ) - // TODO: Change all if statements with if authType open, if yes then don't show password if (wifiData.password.isNotEmpty()) { NfcPasswordRow(title = stringResource(id = R.string.password_title)) } From 5d81c86b7a6d425aa8213541dfcbfd4d7ae60726 Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 30 May 2024 16:34:14 +0200 Subject: [PATCH 39/78] Fixed padding --- .../provisioner/feature/nfc/view/NfcScreen.kt | 84 ++++++++++--------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt index 708f44f8..cc0f8d65 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt @@ -119,49 +119,53 @@ internal fun NfcScreen() { state = WizardStepState.CURRENT, showVerticalDivider = false, ) { - when (val e = nfcScanEvent) { - is Error -> { - // Show the error message. - ProgressItem( - text = stringResource(id = R.string.write_failed), - status = ProgressItemStatus.ERROR - ) - Text( - text = if (e.message.length > 35) e.message.slice(0..35) else e.message, - modifier = Modifier - .alpha(0.7f) - .padding(start = 40.dp), - style = MaterialTheme.typography.bodySmall, - ) - } + Column( + modifier = Modifier.padding(start = 8.dp) + ) { + when (val e = nfcScanEvent) { + is Error -> { + // Show the error message. + ProgressItem( + text = stringResource(id = R.string.write_failed), + status = ProgressItemStatus.ERROR + ) + Text( + text = if (e.message.length > 35) e.message.slice(0..35) else e.message, + modifier = Modifier + .alpha(0.7f) + .padding(start = 40.dp), + style = MaterialTheme.typography.bodySmall, + ) + } - Loading -> { - // Show the loading indicator. - ProgressItem( - text = stringResource(id = R.string.discovering_tag), - status = ProgressItemStatus.WORKING - ) - } + Loading -> { + // Show the loading indicator. + ProgressItem( + text = stringResource(id = R.string.discovering_tag), + status = ProgressItemStatus.WORKING + ) + } - Success -> { - ProgressItem( - text = stringResource(id = R.string.write_success), - status = ProgressItemStatus.SUCCESS - ) - Text( - text = stringResource(id = R.string.success_des), - modifier = Modifier - .alpha(0.7f) - .padding(start = 40.dp), - style = MaterialTheme.typography.bodySmall, - ) - } + Success -> { + ProgressItem( + text = stringResource(id = R.string.write_success), + status = ProgressItemStatus.SUCCESS + ) + Text( + text = stringResource(id = R.string.success_des), + modifier = Modifier + .alpha(0.7f) + .padding(start = 40.dp), + style = MaterialTheme.typography.bodySmall, + ) + } - null -> { - ProgressItem( - text = stringResource(id = R.string.tap_nfc_tag), - status = ProgressItemStatus.WORKING - ) + null -> { + ProgressItem( + text = stringResource(id = R.string.tap_nfc_tag), + status = ProgressItemStatus.WORKING + ) + } } } } From 4f76a3d53e0a22e029c1968da730e597450db3bc Mon Sep 17 00:00:00 2001 From: hiar Date: Mon, 3 Jun 2024 09:57:51 +0200 Subject: [PATCH 40/78] Improved encryption and authmode implementation --- .../feature/nfc/data/WifiAuthType.kt | 44 ++++++++++--------- .../nfc/uicomponent/AddWifiManuallyDialog.kt | 6 +-- .../provisioner/nfc/NdefMessageBuilder.kt | 8 ++-- .../provisioner/nfc/domain/EncryptionMode.kt | 44 ++++++++++--------- 4 files changed, 54 insertions(+), 48 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt index 79888802..f8b7e882 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt @@ -17,7 +17,8 @@ object WifiAuthType { private const val WPA2_EAP = "WPA2-EAP" private const val WPA3_PSK = "WPA3-PSK" - private val securityTypes = listOf(OPEN, WEP, WPA_PSK, WPA2_PSK, WPA_WPA2_PSK, WPA2_EAP, WPA3_PSK) + private val securityTypes = + listOf(OPEN, WEP, WPA_PSK, WPA2_PSK, WPA_WPA2_PSK, WPA2_EAP, WPA3_PSK) /** * @return The security of a given [ScanResult]. @@ -70,7 +71,7 @@ object WifiAuthType { enum class AuthMode(val id: Int) { UNKNOWN(-1), OPEN(0), - WEP(1), + WEP(1), // Shared WPA_PSK(2), WPA2_EAP(3), WPA3_PSK(4), // WPA3-Personal (SAE) password) @@ -84,24 +85,27 @@ enum class AuthMode(val id: Int) { PASSPOINT_R3(12), DPP(13); // Security type for Easy Connect (DPP) network + override fun toString(): String { + return when (this) { + UNKNOWN -> "Unknown" + OPEN -> "Open" + WEP -> "WEP" + WPA_PSK -> "WPA-Personal" + WPA2_EAP -> "WPA2-Enterprise" + WPA3_PSK -> "WPA3-Personal" + EAP_WPA3_ENTERPRISE_192_BIT -> "EAP-WPA3-ENTERPRISE-192-BIT" + OWE -> "OWE" + WAPI_PSK -> "WAPI-Personal" + WAPI_CERT -> "WAPI-CERT" + EAP_WPA3_ENTERPRISE -> "EAP-WPA3-ENTERPRISE" + OSEN -> "OSEN" + PASSPOINT_R1_R2 -> "PASSPOINT-R1-R2" + PASSPOINT_R3 -> "PASSPOINT-R3" + DPP -> "DPP" + } + } + companion object { - private val stringMappings = mapOf( - UNKNOWN to "Unknown", - OPEN to "Open", - WEP to "WEP", - WPA_PSK to "WPA-Personal", - WPA2_EAP to "WPA2-Enterprise", - WPA3_PSK to "WPA3-Personal", - EAP_WPA3_ENTERPRISE_192_BIT to "EAP-WPA3-ENTERPRISE-192-BIT", - OWE to "OWE", - WAPI_PSK to "WAPI-Personal", - WAPI_CERT to "WAPI-CERT", - EAP_WPA3_ENTERPRISE to "EAP-WPA3-ENTERPRISE", - OSEN to "OSEN", - PASSPOINT_R1_R2 to "PASSPOINT-R1-R2", - PASSPOINT_R3 to "PASSPOINT-R3", - DPP to "DPP" - ) /** * Get the authentication mode of a given security type. @@ -125,7 +129,7 @@ enum class AuthMode(val id: Int) { * Note: This is for Build version sdk Tiramisu (API 31) and above. */ private fun authModeTOString(authMode: AuthMode): String { - return stringMappings[authMode] ?: "Unknown" + return authMode.toString() } } } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt index fe84e448..2bda5d90 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt @@ -41,8 +41,8 @@ internal fun AddWifiManuallyDialog( var ssid by rememberSaveable { mutableStateOf("") } var password by rememberSaveable { mutableStateOf("") } var showPassword by rememberSaveable { mutableStateOf(false) } - var authMode by rememberSaveable { mutableStateOf("") } - var encryptionMode by rememberSaveable { mutableStateOf("") } + var authMode by rememberSaveable { mutableStateOf("WPA2-Personal") } // default to WPA2-Personal. + var encryptionMode by rememberSaveable { mutableStateOf(EncryptionMode.AES.toString()) } // default to AES. var isSsidEmpty by rememberSaveable { mutableStateOf(false) } @@ -75,7 +75,7 @@ internal fun AddWifiManuallyDialog( // Show the encryption dropdown. DropdownView( - items = EncryptionMode.getEncryptionList(), + items = EncryptionMode.entries.map { it.toString() }, label = stringResource(id = R.string.encryption), placeholder = stringResource(id = R.string.encryption_placeholder), defaultSelectedItem = encryptionMode diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt index 4e8df1e2..491a387c 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt @@ -129,10 +129,10 @@ class NdefMessageBuilder @Inject constructor() { return ENC_TYPE_AES } return when (enc) { - EncryptionMode.WEP.name -> ENC_TYPE_WEP - EncryptionMode.TKIP.name -> ENC_TYPE_TKIP - EncryptionMode.AES.name -> ENC_TYPE_AES - EncryptionMode.AES_TKIP.name, "AES/TKIP" -> ENC_TYPE_AES_TKIP + EncryptionMode.WEP.toString() -> ENC_TYPE_WEP + EncryptionMode.TKIP.toString() -> ENC_TYPE_TKIP + EncryptionMode.AES.toString() -> ENC_TYPE_AES + EncryptionMode.AES_TKIP.toString() -> ENC_TYPE_AES_TKIP else -> ENC_TYPE_NONE } } diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt index 1eafc444..2ef2b66d 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt @@ -12,11 +12,26 @@ enum class EncryptionMode { AES, AES_TKIP; + override fun toString() = when (this) { + NONE -> "NONE" + WEP -> "WEP" + TKIP -> "TKIP" + AES -> "AES" + AES_TKIP -> "AES/TKIP" + + } + companion object { /** * Returns the encryption mode of the given scan result. * + * Examples of capabilities: + * 1. `[WPA2-PSK-CCMP][ESS]` + * 2. `[WPA2-PSK-CCMP+TKIP][ESS]` + * 3. `[WPA-PSK-CCMP+TKIP][WPA2-PSK-CCMP+TKIP][ESS]` + * [see more](https://stackoverflow.com/questions/11956874/scanresult-capabilities-interpretation), [here](https://security.stackexchange.com/questions/229986/does-wpa2-use-tkip-or-not), + * [here](https://developer.android.com/reference/kotlin/android/net/wifi/ScanResult#capabilities) * @param result the scan result. * @return the encryption mode. */ @@ -27,32 +42,19 @@ enum class EncryptionMode { .toTypedArray() val auth = capabilities[0] + "-" + capabilities[1] val encryptionMode = when { - auth.contains("WPA2-PSK") -> AES + auth.contains("WPA2-PSK") || + auth.contains("WPA3-PSK") || + auth.contains("WPA2-EAP") -> AES + auth.contains("WPA-PSK") -> TKIP - auth.contains("WPA2-EAP") -> AES - auth.contains("WPA-EAP") -> AES_TKIP // TODO: Verify this. - auth.contains("WPA/WPA2-PSK") -> AES_TKIP + auth.contains("WPA-EAP") || + auth.contains("WPA/WPA2-PSK") -> AES_TKIP + auth.contains("WEP") -> WEP - auth.contains("Open") -> NONE - auth.contains("WPA3-PSK") -> AES else -> NONE } - return encryptionMode.name - } - - // Encryption modes to be displayed in the UI - private val encryptionModesToUi = mapOf( - "NONE" to NONE, - "WEP" to WEP, - "TKIP" to TKIP, - "AES" to AES, - "AES/TKIP" to AES_TKIP - ) - - /** Returns the list of encryption modes to be displayed in the UI. */ - fun getEncryptionList(): List { - return encryptionModesToUi.keys.toList() + return encryptionMode.toString() } } } From 5a9647125b71359001192db623854fb79989c052 Mon Sep 17 00:00:00 2001 From: hiar Date: Mon, 3 Jun 2024 15:56:15 +0200 Subject: [PATCH 41/78] Created ProvisionSection view --- app/build.gradle.kts | 1 + .../android/wifi/provisioner/HomeScreen.kt | 33 +++++++--- .../android/wifi/provisioner/MainActivity.kt | 2 + app/src/main/res/values/strings.xml | 13 ++++ .../ble/sections/ProvisionOverBleSection.kt | 40 ------------- feature/ble/src/main/res/values/strings.xml | 5 +- feature/nfc/src/main/res/values/strings.xml | 6 +- .../softap/view/ProvisionOverWifiSection.kt | 56 ----------------- .../softap/src/main/res/values/strings.xml | 4 -- .../ui/view/section/ProvisionSection.kt | 60 +++++++++++++++++++ 10 files changed, 103 insertions(+), 117 deletions(-) delete mode 100644 feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/ProvisionOverBleSection.kt delete mode 100644 feature/softap/src/main/java/no/nordicsemi/android/wifi/provisioner/softap/view/ProvisionOverWifiSection.kt create mode 100644 feature/ui/src/main/java/no/nordicsemi/android/wifi/provisioner/ui/view/section/ProvisionSection.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c02bcbbe..3a958dac 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(project(":feature:ui")) implementation(project(":lib:ble")) implementation(project(":lib:softap")) + implementation(project(":feature:ui")) implementation(libs.androidx.compose.ui) diff --git a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt index 953339a7..3a3fe9b3 100644 --- a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt +++ b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt @@ -46,7 +46,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -69,10 +70,10 @@ import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewMod import no.nordicsemi.android.common.theme.view.NordicAppBar import no.nordicsemi.android.wifi.provisioner.app.BuildConfig import no.nordicsemi.android.wifi.provisioner.app.R -import no.nordicsemi.android.wifi.provisioner.ble.sections.ProvisionOverBleSection import no.nordicsemi.android.wifi.provisioner.ble.view.BleDestination -import no.nordicsemi.android.wifi.provisioner.softap.view.ProvisionOverWifiSection +import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcProvisionerDestinationId import no.nordicsemi.android.wifi.provisioner.softap.view.SoftApDestination +import no.nordicsemi.android.wifi.provisioner.ui.view.section.ProvisionSection @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -96,6 +97,7 @@ fun HomeScreen() { modifier = Modifier .fillMaxSize() .padding(innerPadding) + .verticalScroll(rememberScrollState()) .consumeWindowInsets(innerPadding) .windowInsetsPadding( WindowInsets.safeDrawing.only( @@ -107,7 +109,7 @@ fun HomeScreen() { Row( modifier = Modifier .fillMaxWidth() - .weight(0.4f, true), + .weight(1f, true), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { @@ -122,14 +124,21 @@ fun HomeScreen() { Column( modifier = Modifier .fillMaxWidth() - .weight(0.4f, true), + .padding(bottom = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - ProvisionOverBleSection { + ProvisionSection( + sectionTitle = stringResource(R.string.provision_over_ble), + sectionRational = stringResource(R.string.provision_over_ble_rationale) + ) { vm.navigateTo(BleDestination) } - ProvisionOverWifiSection { + + ProvisionSection( + sectionTitle = stringResource(R.string.provision_over_wifi), + sectionRational = stringResource(R.string.provision_over_wifi_rationale) + ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { vm.navigateTo(SoftApDestination) } else { @@ -141,9 +150,17 @@ fun HomeScreen() { } } } + ProvisionSection( + sectionTitle = stringResource(R.string.provision_over_nfc), + sectionRational = stringResource(R.string.provision_over_nfc_rationale) + ) { + vm.navigateTo(NfcProvisionerDestinationId) + } } Row( - modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), horizontalArrangement = Arrangement.Center, ) { Text( diff --git a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt index 38194797..bfbe145c 100644 --- a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt +++ b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt @@ -37,6 +37,7 @@ import android.os.Bundle import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.ui.Modifier @@ -51,6 +52,7 @@ import no.nordicsemi.android.wifi.provisioner.softap.view.SoftApProvisionerDesti @AndroidEntryPoint class MainActivity : NordicActivity() { + @RequiresApi(Build.VERSION_CODES.M) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 48f49967..82a33635 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -119,4 +119,17 @@ RSSI This feature requires an Android device running Android 10 or above. Version: %s (%s) + + Provision over Bluetooth LE + This mode uses secure Bluetooth LE link to transfer + Wi-Fi credentials to the provisionee and verify provisioning status. + + "Provision over Wi-Fi" + This mode uses temporary a Wi-Fi network (SoftAP) + created by the provisionee to send Wi-Fi credentials. Communication is encrypted using TLS. + + Provision over NFC + This mode allows provisioning a nRF700x device to a + Wi-Fi network by providing the Wi-Fi credentials via NFC. + \ No newline at end of file diff --git a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/ProvisionOverBleSection.kt b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/ProvisionOverBleSection.kt deleted file mode 100644 index d60c62f8..00000000 --- a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/ProvisionOverBleSection.kt +++ /dev/null @@ -1,40 +0,0 @@ -package no.nordicsemi.android.wifi.provisioner.ble.sections - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.wifi.provisioner.feature.ble.R -import no.nordicsemi.android.wifi.provisioner.ui.view.section.SectionTitle - - -@Composable -fun ProvisionOverBleSection(onClick: () -> Unit) { - OutlinedCard( - modifier = Modifier - .widthIn(max = 600.dp) - .padding(all = 8.dp) - .clickable(onClick = onClick) - ) { - Column(modifier = Modifier.padding(all = 16.dp)) { - SectionTitle(text = stringResource( - R.string.provision_over_ble) - ) - Spacer(modifier = Modifier.size(8.dp)) - Text( - text = stringResource(R.string.provision_over_ble_rationale), - style = MaterialTheme.typography.bodyMedium - ) - } - } -} \ No newline at end of file diff --git a/feature/ble/src/main/res/values/strings.xml b/feature/ble/src/main/res/values/strings.xml index da9c95d7..3648d623 100644 --- a/feature/ble/src/main/res/values/strings.xml +++ b/feature/ble/src/main/res/values/strings.xml @@ -117,8 +117,5 @@ Persistent storage - Provision over Bluetooth LE - This mode uses secure Bluetooth LE link to transfer - Wi-Fi credentials to the provisionee and verify provisioning status. - + \ No newline at end of file diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index 82de3da2..a0514f4a 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -3,10 +3,6 @@ Provision over NFC Wi-Fi networks Publish - Provision over NFC - This mode allows provisioning a nRF700x device to a - Wi-Fi network by providing the Wi-Fi credentials via NFC. - Enter Wi-Fi credentials Enter the Wi-Fi credentials manually. @@ -44,7 +40,7 @@ Enter password - Wi-fi record + Wi-fi Record SSID Password Encryption diff --git a/feature/softap/src/main/java/no/nordicsemi/android/wifi/provisioner/softap/view/ProvisionOverWifiSection.kt b/feature/softap/src/main/java/no/nordicsemi/android/wifi/provisioner/softap/view/ProvisionOverWifiSection.kt deleted file mode 100644 index 1d05d444..00000000 --- a/feature/softap/src/main/java/no/nordicsemi/android/wifi/provisioner/softap/view/ProvisionOverWifiSection.kt +++ /dev/null @@ -1,56 +0,0 @@ -package no.nordicsemi.android.wifi.provisioner.softap.view - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.wifi.provisioner.feature.softap.R -import no.nordicsemi.android.wifi.provisioner.ui.view.section.SectionTitle - -@Composable -fun ProvisionOverWifiSection(onClick: () -> Unit) { - OutlinedCard( - modifier = Modifier - .widthIn(max = 600.dp) - .padding(all = 8.dp) - .clickable(onClick = onClick) - ) { - Column(modifier = Modifier.padding(all = 16.dp)) { - SectionTitle(text = stringResource(R.string.provision_over_wifi)) - Spacer(modifier = Modifier.size(8.dp)) - Text( - text = stringResource(R.string.provision_over_wifi_rationale), - style = MaterialTheme.typography.bodyMedium - ) - } - } -} - -@Composable -fun ProvisionOverNfc(onClick: () -> Unit) { - OutlinedCard( - modifier = Modifier - .padding(all = 8.dp) - .clickable(onClick = onClick) - ) { - Column(modifier = Modifier.padding(all = 16.dp)) { - SectionTitle(text = stringResource(R.string.provision_over_nfc)) - Spacer(modifier = Modifier.size(8.dp)) - Text( - text = stringResource(R.string.provision_over_nfc_rationale), - style = MaterialTheme.typography.bodyMedium - ) - } - } -} \ No newline at end of file diff --git a/feature/softap/src/main/res/values/strings.xml b/feature/softap/src/main/res/values/strings.xml index e914b1a7..41b89e48 100644 --- a/feature/softap/src/main/res/values/strings.xml +++ b/feature/softap/src/main/res/values/strings.xml @@ -1,6 +1,5 @@ - "Provision over Wi-Fi" Connect to Device Enter the SSID of the SoftAP you want to connect to. \n\nNote: Make sure the nRF 700x device is powered on and in range. @@ -41,9 +40,6 @@ Wi-Fi credentials sent Send Wi-Fi credentials Optionally verify provisioning state - This mode uses temporary a Wi-Fi network (SoftAP) - created by the provisionee to send Wi-Fi credentials. Communication is encrypted using TLS. - Locating provisioned device… Verification completed Please enable Wi-Fi! diff --git a/feature/ui/src/main/java/no/nordicsemi/android/wifi/provisioner/ui/view/section/ProvisionSection.kt b/feature/ui/src/main/java/no/nordicsemi/android/wifi/provisioner/ui/view/section/ProvisionSection.kt new file mode 100644 index 00000000..e27da706 --- /dev/null +++ b/feature/ui/src/main/java/no/nordicsemi/android/wifi/provisioner/ui/view/section/ProvisionSection.kt @@ -0,0 +1,60 @@ +package no.nordicsemi.android.wifi.provisioner.ui.view.section + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.common.theme.NordicTheme + +/** + * A composable that displays a section with a title and a rationale. + * + * @param sectionTitle The title of the section. + * @param sectionRational The rationale of the section. + * @param onClick The action to be performed when the section is clicked. + */ +@Composable +fun ProvisionSection( + sectionTitle: String, + sectionRational: String, + onClick: () -> Unit +) { + OutlinedCard( + modifier = Modifier + .widthIn(max = 600.dp) + .fillMaxWidth() + .padding(8.dp) + .clickable { onClick() } + ) { + Column(modifier = Modifier.padding(16.dp)) { + SectionTitle(text = sectionTitle) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = sectionRational, + style = MaterialTheme.typography.bodyMedium + ) + } + } +} + +@Preview +@Composable +private fun ProvisionSectionPreview() { + NordicTheme { + ProvisionSection( + sectionTitle = "Provision over BLE", + sectionRational = "Provision over BLE rationale.", + onClick = {} + ) + } +} \ No newline at end of file From 0f600f09b7331afd358295641c14640894b20678 Mon Sep 17 00:00:00 2001 From: hiar Date: Mon, 3 Jun 2024 16:03:17 +0200 Subject: [PATCH 42/78] Missing strings --- feature/softap/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/feature/softap/src/main/res/values/strings.xml b/feature/softap/src/main/res/values/strings.xml index 41b89e48..c7b5b10e 100644 --- a/feature/softap/src/main/res/values/strings.xml +++ b/feature/softap/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ + Provision over Wi-Fi Connect to Device Enter the SSID of the SoftAP you want to connect to. \n\nNote: Make sure the nRF 700x device is powered on and in range. From 79ade19535ac21efc9e1bcc1c9484e6caf14346d Mon Sep 17 00:00:00 2001 From: hiar Date: Mon, 3 Jun 2024 16:35:59 +0200 Subject: [PATCH 43/78] Fixed padding --- .../provisioner/feature/nfc/view/NfcScreen.kt | 20 ++++++++++--------- feature/nfc/src/main/res/values/strings.xml | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt index cc0f8d65..9bff4592 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt @@ -119,21 +119,20 @@ internal fun NfcScreen() { state = WizardStepState.CURRENT, showVerticalDivider = false, ) { - Column( - modifier = Modifier.padding(start = 8.dp) - ) { + Column { when (val e = nfcScanEvent) { is Error -> { // Show the error message. ProgressItem( text = stringResource(id = R.string.write_failed), - status = ProgressItemStatus.ERROR + status = ProgressItemStatus.ERROR, + iconRightPadding = 24.dp, ) Text( text = if (e.message.length > 35) e.message.slice(0..35) else e.message, modifier = Modifier .alpha(0.7f) - .padding(start = 40.dp), + .padding(start = 48.dp), style = MaterialTheme.typography.bodySmall, ) } @@ -142,20 +141,22 @@ internal fun NfcScreen() { // Show the loading indicator. ProgressItem( text = stringResource(id = R.string.discovering_tag), - status = ProgressItemStatus.WORKING + status = ProgressItemStatus.WORKING, + iconRightPadding = 24.dp, ) } Success -> { ProgressItem( text = stringResource(id = R.string.write_success), - status = ProgressItemStatus.SUCCESS + status = ProgressItemStatus.SUCCESS, + iconRightPadding = 24.dp, ) Text( text = stringResource(id = R.string.success_des), modifier = Modifier .alpha(0.7f) - .padding(start = 40.dp), + .padding(start = 48.dp), style = MaterialTheme.typography.bodySmall, ) } @@ -163,7 +164,8 @@ internal fun NfcScreen() { null -> { ProgressItem( text = stringResource(id = R.string.tap_nfc_tag), - status = ProgressItemStatus.WORKING + status = ProgressItemStatus.WORKING, + iconRightPadding = 24.dp, ) } } diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index a0514f4a..3c646803 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -40,7 +40,7 @@ Enter password - Wi-fi Record + Wi-Fi record SSID Password Encryption From 3f1b7d78d1b16602cf3d0b30c282986922e601c5 Mon Sep 17 00:00:00 2001 From: Aleksander Nowakowski Date: Tue, 4 Jun 2024 12:21:50 +0200 Subject: [PATCH 44/78] Polishing --- .../android/wifi/provisioner/HomeScreen.kt | 2 +- app/src/main/res/values/strings.xml | 85 +---------------- .../ble/sections/ActionButtonSection.kt | 2 +- .../provisioner/ble/sections/DeviceSection.kt | 2 +- .../ble/sections/DisconnectedDeviceStatus.kt | 2 +- .../ble/sections/PasswordSection.kt | 2 +- .../ble/sections/ProvisioningSection.kt | 2 +- .../ble/sections/UnprovisioningSection.kt | 5 +- .../provisioner/ble/sections/WifiSection.kt | 2 +- .../wifi/provisioner/ble/view/UiMapper.kt | 2 +- feature/nfc/build.gradle.kts | 1 + feature/ui/src/main/res/values/strings.xml | 93 ++----------------- 12 files changed, 20 insertions(+), 180 deletions(-) diff --git a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt index 3a3fe9b3..70293b25 100644 --- a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt +++ b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt @@ -145,7 +145,7 @@ fun HomeScreen() { scope.launch { snackbarHostState.showSnackbar( message = context.getString(R.string.error_softap_not_supported), - actionLabel = context.getString(R.string.dismiss) + actionLabel = context.getString(no.nordicsemi.android.wifi.provisioner.ui.R.string.dismiss) ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 82a33635..ddc8fd9c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,91 +32,8 @@ nRF Wi-Fi Provisioner - Change An icon of nRF70 Series - Device status - Provisioning data - Upload status - - Unknown error - - Start - Next device - Finish - Set password - Password - **** **** - Dismiss - Accept - Clear - - Device status - Disconnected - Wi-Fi status - Start provisioning - Version - Scanning error - Unprovision - Select password - Provision - Provisioning status - Unrovisioning status - Success - - - Authentication - Association - Obtaining IP - Connected - Disconnected - Unprovisioned - Error occurred during provisioning. - Icon indicating wifi and it\'s authentication method. - - Wi-Fi - - IPv4: %s - SSID: %s - BSSID: %s - Band: %s - Band: %s, Channel: %s - Channel: %s - 2.4 GHz - 5 GHz - Any - - Authentication error. - The specified network could not be find. - Timeout occurred. - Could not obtain IP from provided provisioning information. - Could not connect to provisioned network. - - Connection info - Wi-Fi info - - Scan params - Passive: %s - Period: %s [ms] - Group channels: %s - - Hide password - Show password - - Authentication - Association - Obtaining IP - Result - Connected - Connection failed - - V %d - - Persistent storage - - Sort by: - Name - RSSI This feature requires an Android device running Android 10 or above. Version: %s (%s) @@ -124,7 +41,7 @@ This mode uses secure Bluetooth LE link to transfer Wi-Fi credentials to the provisionee and verify provisioning status. - "Provision over Wi-Fi" + Provision over Wi-Fi This mode uses temporary a Wi-Fi network (SoftAP) created by the provisionee to send Wi-Fi credentials. Communication is encrypted using TLS. diff --git a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/ActionButtonSection.kt b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/ActionButtonSection.kt index 82ac4a99..3539dd43 100644 --- a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/ActionButtonSection.kt +++ b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/ActionButtonSection.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import no.nordicsemi.android.wifi.provisioner.ble.view.BleViewEntity import no.nordicsemi.android.wifi.provisioner.ble.view.OnUnprovisionEvent -import no.nordicsemi.android.wifi.provisioner.ui.R +import no.nordicsemi.android.wifi.provisioner.feature.ble.R import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.OnFinishedEvent import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.OnProvisionClickEvent import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.OnProvisionNextDeviceEvent diff --git a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/DeviceSection.kt b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/DeviceSection.kt index 335d8165..816c46b9 100644 --- a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/DeviceSection.kt +++ b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/DeviceSection.kt @@ -38,8 +38,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import no.nordicsemi.android.common.theme.NordicTheme +import no.nordicsemi.android.wifi.provisioner.feature.ble.R import no.nordicsemi.android.wifi.provisioner.ui.ClickableDataItem -import no.nordicsemi.android.wifi.provisioner.ui.R import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.OnProvisionNextDeviceEvent import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.OnSelectDeviceClickEvent import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.ProvisioningViewEvent diff --git a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/DisconnectedDeviceStatus.kt b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/DisconnectedDeviceStatus.kt index c385c7e6..a96fa0f3 100644 --- a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/DisconnectedDeviceStatus.kt +++ b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/DisconnectedDeviceStatus.kt @@ -36,7 +36,7 @@ import androidx.compose.material.icons.outlined.LinkOff import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import no.nordicsemi.android.wifi.provisioner.ui.DataItem -import no.nordicsemi.android.wifi.provisioner.ui.R +import no.nordicsemi.android.wifi.provisioner.feature.ble.R @Composable fun DisconnectedDeviceStatus() { diff --git a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/PasswordSection.kt b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/PasswordSection.kt index bea0d990..996d147c 100644 --- a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/PasswordSection.kt +++ b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/PasswordSection.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.res.stringResource import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.ProvisioningViewEvent import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.OnShowPasswordDialog import no.nordicsemi.android.wifi.provisioner.ui.ClickableDataItem -import no.nordicsemi.android.wifi.provisioner.ui.R +import no.nordicsemi.android.wifi.provisioner.feature.ble.R @Composable fun PasswordSection(isEditable: Boolean = false, onEvent: (ProvisioningViewEvent) -> Unit) { diff --git a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/ProvisioningSection.kt b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/ProvisioningSection.kt index 7032c474..b4c80d66 100644 --- a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/ProvisioningSection.kt +++ b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/ProvisioningSection.kt @@ -49,7 +49,7 @@ import no.nordicsemi.android.common.theme.view.ProgressItemStatus import no.nordicsemi.kotlin.wifi.provisioner.domain.WifiConnectionStateDomain import no.nordicsemi.android.wifi.provisioner.ui.DataItem import no.nordicsemi.android.wifi.provisioner.ui.LoadingItem -import no.nordicsemi.android.wifi.provisioner.ui.R +import no.nordicsemi.android.wifi.provisioner.feature.ble.R import no.nordicsemi.android.wifi.provisioner.ble.view.toDisplayString import no.nordicsemi.kotlin.wifi.provisioner.domain.resource.Error import no.nordicsemi.kotlin.wifi.provisioner.domain.resource.Loading diff --git a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/UnprovisioningSection.kt b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/UnprovisioningSection.kt index 6c1a450e..0d10dc20 100644 --- a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/UnprovisioningSection.kt +++ b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/UnprovisioningSection.kt @@ -41,7 +41,7 @@ import androidx.compose.ui.unit.dp import no.nordicsemi.android.wifi.provisioner.ui.DataItem import no.nordicsemi.android.wifi.provisioner.ui.ErrorDataItem import no.nordicsemi.android.wifi.provisioner.ui.LoadingItem -import no.nordicsemi.android.wifi.provisioner.ui.R +import no.nordicsemi.android.wifi.provisioner.feature.ble.R import no.nordicsemi.kotlin.wifi.provisioner.domain.resource.Loading import no.nordicsemi.kotlin.wifi.provisioner.domain.resource.Resource import no.nordicsemi.kotlin.wifi.provisioner.domain.resource.Success @@ -56,11 +56,10 @@ fun UnprovisioningSection(status: Resource) { } } - @Composable private fun ErrorItem(error: Throwable) { ErrorDataItem( - iconRes = R.drawable.ic_upload_wifi, + iconRes = no.nordicsemi.android.wifi.provisioner.ui.R.drawable.ic_upload_wifi, title = stringResource(id = R.string.unprovision_status), error = error ) diff --git a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/WifiSection.kt b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/WifiSection.kt index 2eaf412f..439fc0ad 100644 --- a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/WifiSection.kt +++ b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/sections/WifiSection.kt @@ -39,7 +39,7 @@ import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.OnSelectWifiEv import no.nordicsemi.kotlin.wifi.provisioner.domain.ScanRecordDomain import no.nordicsemi.kotlin.wifi.provisioner.feature.common.WifiData import no.nordicsemi.android.wifi.provisioner.ui.ClickableDataItem -import no.nordicsemi.android.wifi.provisioner.ui.R +import no.nordicsemi.android.wifi.provisioner.feature.ble.R import no.nordicsemi.android.wifi.provisioner.ui.mapping.toImageVector @Composable diff --git a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/view/UiMapper.kt b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/view/UiMapper.kt index 36cfe829..13a3d6a7 100644 --- a/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/view/UiMapper.kt +++ b/feature/ble/src/main/java/no/nordicsemi/android/wifi/provisioner/ble/view/UiMapper.kt @@ -41,7 +41,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import no.nordicsemi.kotlin.wifi.provisioner.domain.WifiConnectionFailureReasonDomain import no.nordicsemi.kotlin.wifi.provisioner.domain.WifiConnectionStateDomain -import no.nordicsemi.android.wifi.provisioner.ui.R +import no.nordicsemi.android.wifi.provisioner.feature.ble.R @Composable fun WifiConnectionStateDomain?.toImageVector() = when (this) { diff --git a/feature/nfc/build.gradle.kts b/feature/nfc/build.gradle.kts index 72c41e55..a8a104b8 100644 --- a/feature/nfc/build.gradle.kts +++ b/feature/nfc/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(libs.androidx.compose.material.iconsExtended) implementation(libs.androidx.lifecycle.runtime.compose) implementation(project(":feature:common")) + implementation(project(":feature:ui")) api(project(":lib:nfc")) api(project(":lib:domain")) // TODD: Remove this once feature:common is utilized in this module. implementation(libs.nordic.permissions.nfc) diff --git a/feature/ui/src/main/res/values/strings.xml b/feature/ui/src/main/res/values/strings.xml index 3ba6bd9d..0359b547 100644 --- a/feature/ui/src/main/res/values/strings.xml +++ b/feature/ui/src/main/res/values/strings.xml @@ -1,54 +1,17 @@ - Change - - Sort by: - Name - RSSI - - Device status - Provisioning data - Upload status - Unknown error + Icon representing data available in the section. - Start - Next device - Finish - Set password - Password - **** **** - Dismiss Accept + Dismiss Clear - Device status - Disconnected - Wi-Fi status - Selected Wi-Fi - Start provisioning - Version - Scanning error - Unprovision - Select password - Provision - Provisioning status - Unrovisioning status - Success - - Icon representing data available in the section. - - Authentication - Association - Obtaining IP - Connected - Disconnected - Unprovisioned - Error occurred during provisioning. - Icon indicating wifi and it\'s authentication method. - - Wi-Fi + Sort by: + Name + RSSI + Select Wi-Fi IPv4: %s SSID: %s BSSID: %s @@ -59,48 +22,8 @@ 5 GHz Any - Authentication error. - The specified network could not be find. - Timeout occurred. - Could not obtain IP from provided provisioning information. - Could not connect to provisioned network. - - Connection info - Wi-Fi info - - Scan params - Passive: %s - Period: %s [ms] - Group channels: %s - + Password + Set password Hide password Show password - - Authentication - Association - Obtaining IP - Result - Connected - Connection failed - - V %d - Select Wi-Fi - - Persistent storage - - Provision over Bluetooth LE - This mode allows provisioning a nRF700x device to a - Wi-Fi network by providing the Wi-Fi credentials, directly connecting to the - device using Bluetooth LE - - Provision over Wi-Fi - This mode allows provisioning a nRF700x device to a - Wi-Fi network by providing the Wi-Fi credentials over Wi-Fi by directly connecting to the - Soft AP advertised by the device. - - - Provision over NFC - This mode allows provisioning a nRF700x device to a - Wi-Fi network by providing the Wi-Fi credentials via NFC. - \ No newline at end of file From 4194902245b37ae88747bd4979635a5b9baaabd8 Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 4 Jun 2024 13:12:05 +0200 Subject: [PATCH 45/78] Rename file --- .../android/wifi/provisioner/HomeScreen.kt | 4 ++-- .../feature/nfc/NfcDestinations.kt | 24 +++++++++---------- .../feature/nfc/view/WifiScannerView.kt | 2 +- .../nfc/viewmodel/NfcManagerViewModel.kt | 4 ++-- .../nfc/viewmodel/NfcProvisioningViewModel.kt | 8 +++---- .../nfc/viewmodel/WifiScannerViewModel.kt | 4 ++-- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt index 70293b25..7ed3a21b 100644 --- a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt +++ b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt @@ -71,7 +71,7 @@ import no.nordicsemi.android.common.theme.view.NordicAppBar import no.nordicsemi.android.wifi.provisioner.app.BuildConfig import no.nordicsemi.android.wifi.provisioner.app.R import no.nordicsemi.android.wifi.provisioner.ble.view.BleDestination -import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcProvisionerDestinationId +import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcDestination import no.nordicsemi.android.wifi.provisioner.softap.view.SoftApDestination import no.nordicsemi.android.wifi.provisioner.ui.view.section.ProvisionSection @@ -154,7 +154,7 @@ fun HomeScreen() { sectionTitle = stringResource(R.string.provision_over_nfc), sectionRational = stringResource(R.string.provision_over_nfc_rationale) ) { - vm.navigateTo(NfcProvisionerDestinationId) + vm.navigateTo(NfcDestination) } } Row( diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/NfcDestinations.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/NfcDestinations.kt index bc938121..266348c4 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/NfcDestinations.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/NfcDestinations.kt @@ -5,30 +5,30 @@ import androidx.annotation.RequiresApi import no.nordicsemi.android.common.navigation.createDestination import no.nordicsemi.android.common.navigation.createSimpleDestination import no.nordicsemi.android.common.navigation.defineDestination -import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.NfcProvisioningScreen +import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.NfcPublishScreen import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.NfcScreen -import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.WifiScannerView +import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.WifiScannerScreen import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData -val NfcProvisionerDestinationId = createSimpleDestination("nfc-provider-destination") -val WifiScannerDestinationId = +val NfcDestination = createSimpleDestination("nfc") +val WifiScannerDestination = createSimpleDestination( name = "wifi-scanner-destination", ) -val NfcDestinationId = - createDestination("provision-over-nfc-destination") +val NfcPublishDestination = + createDestination("publish-destination") @RequiresApi(Build.VERSION_CODES.M) val NfcProvisionerDestinations = listOf( - defineDestination(NfcProvisionerDestinationId) { - NfcProvisioningScreen() + defineDestination(NfcDestination) { + NfcScreen() }, - defineDestination(WifiScannerDestinationId) { - WifiScannerView() + defineDestination(WifiScannerDestination) { + WifiScannerScreen() }, - defineDestination(NfcDestinationId) { - NfcScreen() + defineDestination(NfcPublishDestination) { + NfcPublishScreen() } ) \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index d52c93bb..d03c945c 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -72,7 +72,7 @@ import no.nordicsemi.android.wifi.provisioner.ui.view.WifiSortView @RequiresApi(Build.VERSION_CODES.M) @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun WifiScannerView() { +internal fun WifiScannerScreen() { val wifiScannerViewModel = hiltViewModel() val onEvent: (WifiScannerViewEvent) -> Unit = { wifiScannerViewModel.onEvent(it) } val viewState by wifiScannerViewModel.viewState.collectAsStateWithLifecycle() diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt index 22961086..4891ef86 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcManagerViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.lifecycle.HiltViewModel import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel -import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcDestinationId +import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcPublishDestination import no.nordicsemi.android.wifi.provisioner.nfc.NdefMessageBuilder import no.nordicsemi.android.wifi.provisioner.nfc.NfcManagerForWifi import javax.inject.Inject @@ -21,7 +21,7 @@ internal class NfcManagerViewModel @Inject constructor( private val navigator: Navigator, savedStateHandle: SavedStateHandle, ) : SimpleNavigationViewModel(navigator, savedStateHandle) { - val wifiData = parameterOf(NfcDestinationId) + val wifiData = parameterOf(NfcPublishDestination) val ndefMessage: NdefMessage = ndefMessageBuilder.createNdefMessage(wifiData) val nfcScanEvent = nfcManagerForWifi.nfcScanEvent diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewModel.kt index 1c759ddf..c066557b 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/NfcProvisioningViewModel.kt @@ -5,8 +5,8 @@ import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcDestinationId -import no.nordicsemi.android.wifi.provisioner.feature.nfc.WifiScannerDestinationId +import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcPublishDestination +import no.nordicsemi.android.wifi.provisioner.feature.nfc.WifiScannerDestination import javax.inject.Inject @RequiresApi(Build.VERSION_CODES.M) @@ -20,13 +20,13 @@ internal class NfcProvisioningViewModel @Inject constructor( fun onEvent(event: NfcProvisioningViewEvent) { when (event) { is OnScanClickEvent -> { - navigator.navigateTo(WifiScannerDestinationId) + navigator.navigateTo(WifiScannerDestination) } OnBackClickEvent -> navigator.navigateUp() is OnAddWifiNetworkClickEvent -> { // Navigate to the NFC screen with the Wi-Fi data. - navigator.navigateTo(NfcDestinationId, event.wifiData) + navigator.navigateTo(NfcPublishDestination, event.wifiData) } } } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index cccabb17..0d8c147a 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcDestinationId +import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcPublishDestination import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState @@ -131,7 +131,7 @@ internal class WifiScannerViewModel @Inject constructor( */ private fun navigateToNfcScan(wifiData: WifiData) { navigator.navigateTo( - NfcDestinationId, + NfcPublishDestination, wifiData ) } From bb4b5cf1f0f6703ebb8ad580d780cc6b42b7e472 Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 4 Jun 2024 13:12:51 +0200 Subject: [PATCH 46/78] Fixed bg color --- .../feature/nfc/view/NfcProvisioningScreen.kt | 127 --------- .../feature/nfc/view/NfcPublishScreen.kt | 189 +++++++++++++ .../provisioner/feature/nfc/view/NfcScreen.kt | 253 ++++++++---------- .../feature/nfc/view/WifiScannerView.kt | 183 +++++++------ 4 files changed, 396 insertions(+), 356 deletions(-) delete mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt deleted file mode 100644 index 6eb01a15..00000000 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcProvisioningScreen.kt +++ /dev/null @@ -1,127 +0,0 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.view - -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Wifi -import androidx.compose.material.icons.filled.WifiFind -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import no.nordicsemi.android.common.theme.view.NordicAppBar -import no.nordicsemi.android.wifi.provisioner.feature.nfc.R -import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.AddWifiManuallyDialog -import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.OutlinedCardItem -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcProvisioningViewEvent -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcProvisioningViewModel -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnAddWifiNetworkClickEvent -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnBackClickEvent -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnScanClickEvent - -@RequiresApi(Build.VERSION_CODES.M) -@OptIn(ExperimentalMaterial3Api::class) -@Composable -internal fun NfcProvisioningScreen() { - val viewModel: NfcProvisioningViewModel = hiltViewModel() - val onEvent: (NfcProvisioningViewEvent) -> Unit = { viewModel.onEvent(it) } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 56.dp) - ) { - NordicAppBar( - text = stringResource(id = R.string.wifi_provision_over_nfc_appbar), - showBackButton = true, - onNavigationButtonClick = { onEvent(OnBackClickEvent) } - ) - // Show the home screen. - var isDialogOpen by rememberSaveable { mutableStateOf(false) } - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.Companion - .padding(8.dp) - ) { - // Show an option to enter the WiFi credentials manually. - OutlinedCardItem( - headline = stringResource(id = R.string.enter_wifi_credentials), - description = { - Text( - text = stringResource(id = R.string.enter_wifi_credentials_des), - style = MaterialTheme.typography.bodyMedium, - ) - }, - icon = Icons.Default.Wifi, - onCardClick = { isDialogOpen = true } - ) { - Spacer(Modifier.weight(1f)) - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - modifier = Modifier - .clip(CircleShape) - .clickable { - isDialogOpen = true - } - .padding(8.dp) - ) - } - - // Show an option to search for a WiFi network. - OutlinedCardItem( - headline = stringResource(id = R.string.search_for_wifi_networks), - description = { - Text( - text = stringResource(id = R.string.search_for_wifi_networks_des), - style = MaterialTheme.typography.bodyMedium, - ) - }, - icon = Icons.Default.WifiFind, - onCardClick = { onEvent(OnScanClickEvent) } - ) { - Spacer(Modifier.weight(1f)) - Icon( - imageVector = Icons.Default.Search, - contentDescription = null, - modifier = Modifier - .clip(CircleShape) - .clickable { onEvent(OnScanClickEvent) } - .padding(8.dp) - ) - } - - if (isDialogOpen) { - // Open a dialog to enter the WiFi credentials manually. - AddWifiManuallyDialog( - onCancelClick = { - isDialogOpen = false - }, - onConfirmClick = { - isDialogOpen = false - onEvent(OnAddWifiNetworkClickEvent(it)) - }, - ) - } - } - } -} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt new file mode 100644 index 00000000..728b0689 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt @@ -0,0 +1,189 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.view + +import android.app.Activity +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Wifi +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.common.permissions.nfc.RequireNfc +import no.nordicsemi.android.common.theme.view.NordicAppBar +import no.nordicsemi.android.common.theme.view.ProgressItem +import no.nordicsemi.android.common.theme.view.ProgressItemStatus +import no.nordicsemi.android.common.theme.view.WizardStepComponent +import no.nordicsemi.android.common.theme.view.WizardStepState +import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.NfcPasswordRow +import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.NfcTextRow +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcManagerViewModel +import no.nordicsemi.android.wifi.provisioner.nfc.Error +import no.nordicsemi.android.wifi.provisioner.nfc.Loading +import no.nordicsemi.android.wifi.provisioner.nfc.Success + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun NfcPublishScreen() { + val nfcManagerVm: NfcManagerViewModel = hiltViewModel() + val context = LocalContext.current + val nfcScanEvent by nfcManagerVm.nfcScanEvent.collectAsStateWithLifecycle() + val ndefMessage = nfcManagerVm.ndefMessage + val wifiData = nfcManagerVm.wifiData + val snackbarHostState = remember { SnackbarHostState() } + + // Handle back navigation. + BackHandler { + nfcManagerVm.onBackNavigation() + } + Scaffold( + contentWindowInsets = WindowInsets(0, 0, 0, 0), + topBar = { + NordicAppBar( + text = stringResource(id = R.string.ndef_publish_appbar), + showBackButton = true, + onNavigationButtonClick = { nfcManagerVm.onBackNavigation() } + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + ) { + RequireNfc { + DisposableEffect(key1 = nfcManagerVm) { + nfcManagerVm.onScan(context as Activity) + onDispose { nfcManagerVm.onPause(context) } + } + OutlinedCard( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(vertical = 16.dp, horizontal = 16.dp) + // Leave more space for the navigation bar. + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + // Show Ndef Record information. + WizardStepComponent( + icon = Icons.Default.Wifi, + title = stringResource(id = R.string.wifi_record), + state = WizardStepState.COMPLETED + ) { + NfcTextRow( + title = stringResource(id = R.string.ssid_title), + text = wifiData.ssid + ) + if (wifiData.password.isNotEmpty()) { + NfcPasswordRow(title = stringResource(id = R.string.password_title)) + } + if (wifiData.authType.isNotEmpty()) { + NfcTextRow( + title = stringResource(id = R.string.authentication_title), + text = wifiData.authType + ) + } + if (wifiData.encryptionMode.isNotEmpty()) { + NfcTextRow( + title = stringResource(id = R.string.encryption_title), + text = wifiData.encryptionMode + ) + } + NfcTextRow( + title = stringResource(id = R.string.message_size), + text = stringResource( + id = R.string.message_size_in_bytes, + ndefMessage.byteArrayLength + ) + ) + } + + WizardStepComponent( + icon = Icons.Default.Edit, + title = stringResource(id = R.string.discover_tag_title), + state = WizardStepState.CURRENT, + showVerticalDivider = false, + ) { + Column { + when (val e = nfcScanEvent) { + is Error -> { + // Show the error message. + ProgressItem( + text = stringResource(id = R.string.write_failed), + status = ProgressItemStatus.ERROR, + iconRightPadding = 24.dp, + ) + Text( + text = if (e.message.length > 35) e.message.slice(0..35) else e.message, + modifier = Modifier + .alpha(0.7f) + .padding(start = 48.dp), + style = MaterialTheme.typography.bodySmall, + ) + } + + Loading -> { + // Show the loading indicator. + ProgressItem( + text = stringResource(id = R.string.discovering_tag), + status = ProgressItemStatus.WORKING, + iconRightPadding = 24.dp, + ) + } + + Success -> { + ProgressItem( + text = stringResource(id = R.string.write_success), + status = ProgressItemStatus.SUCCESS, + iconRightPadding = 24.dp, + ) + Text( + text = stringResource(id = R.string.success_des), + modifier = Modifier + .alpha(0.7f) + .padding(start = 48.dp), + style = MaterialTheme.typography.bodySmall, + ) + } + + null -> { + ProgressItem( + text = stringResource(id = R.string.tap_nfc_tag), + status = ProgressItemStatus.WORKING, + iconRightPadding = 24.dp, + ) + } + } + } + } + } + } + } + } + } +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt index 9bff4592..81fd18a6 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt @@ -1,177 +1,138 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.view -import android.app.Activity -import androidx.activity.compose.BackHandler +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Wifi +import androidx.compose.material.icons.filled.WifiFind import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import no.nordicsemi.android.common.permissions.nfc.RequireNfc import no.nordicsemi.android.common.theme.view.NordicAppBar -import no.nordicsemi.android.common.theme.view.ProgressItem -import no.nordicsemi.android.common.theme.view.ProgressItemStatus -import no.nordicsemi.android.common.theme.view.WizardStepComponent -import no.nordicsemi.android.common.theme.view.WizardStepState import no.nordicsemi.android.wifi.provisioner.feature.nfc.R -import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.NfcPasswordRow -import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.NfcTextRow -import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcManagerViewModel -import no.nordicsemi.android.wifi.provisioner.nfc.Error -import no.nordicsemi.android.wifi.provisioner.nfc.Loading -import no.nordicsemi.android.wifi.provisioner.nfc.Success +import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.AddWifiManuallyDialog +import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.OutlinedCardItem +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcProvisioningViewEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcProvisioningViewModel +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnAddWifiNetworkClickEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnBackClickEvent +import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnScanClickEvent +@RequiresApi(Build.VERSION_CODES.M) @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun NfcScreen() { - val nfcManagerVm: NfcManagerViewModel = hiltViewModel() - val context = LocalContext.current - val nfcScanEvent by nfcManagerVm.nfcScanEvent.collectAsStateWithLifecycle() - val ndefMessage = nfcManagerVm.ndefMessage - val wifiData = nfcManagerVm.wifiData + val viewModel: NfcProvisioningViewModel = hiltViewModel() + val onEvent: (NfcProvisioningViewEvent) -> Unit = { viewModel.onEvent(it) } + val snackbarHostState = remember { SnackbarHostState() } - // Handle back navigation. - BackHandler { - nfcManagerVm.onBackNavigation() - } - Column( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 56.dp) - ) { - NordicAppBar( - text = stringResource(id = R.string.ndef_publish_appbar), - showBackButton = true, - onNavigationButtonClick = { nfcManagerVm.onBackNavigation() } - ) - RequireNfc { - DisposableEffect(key1 = nfcManagerVm) { - nfcManagerVm.onScan(context as Activity) - onDispose { nfcManagerVm.onPause(context) } - } - OutlinedCard( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .widthIn(max = 600.dp) - .padding(16.dp) - // Leave more space for the navigation bar. - .padding(bottom = 16.dp) + Scaffold( + contentWindowInsets = WindowInsets(0, 0, 0, 0), + topBar = { + NordicAppBar( + text = stringResource(id = R.string.wifi_provision_over_nfc_appbar), + showBackButton = true, + onNavigationButtonClick = { onEvent(OnBackClickEvent) } + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + // Show the home screen. + var isDialogOpen by rememberSaveable { mutableStateOf(false) } + + // Show an option to enter the WiFi credentials manually. + OutlinedCardItem( + headline = stringResource(id = R.string.enter_wifi_credentials), + description = { + Text( + text = stringResource(id = R.string.enter_wifi_credentials_des), + style = MaterialTheme.typography.bodyMedium, + ) + }, + icon = Icons.Default.Wifi, + onCardClick = { isDialogOpen = true } ) { - Column( + Spacer(Modifier.weight(1f)) + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, modifier = Modifier - .padding(16.dp) - ) { - // Show Ndef Record information. - WizardStepComponent( - icon = Icons.Default.Wifi, - title = stringResource(id = R.string.wifi_record), - state = WizardStepState.COMPLETED - ) { - NfcTextRow( - title = stringResource(id = R.string.ssid_title), - text = wifiData.ssid - ) - if (wifiData.password.isNotEmpty()) { - NfcPasswordRow(title = stringResource(id = R.string.password_title)) + .clip(CircleShape) + .clickable { + isDialogOpen = true } - if (wifiData.authType.isNotEmpty()) { - NfcTextRow( - title = stringResource(id = R.string.authentication_title), - text = wifiData.authType - ) - } - if (wifiData.encryptionMode.isNotEmpty()) { - NfcTextRow( - title = stringResource(id = R.string.encryption_title), - text = wifiData.encryptionMode - ) - } - NfcTextRow( - title = stringResource(id = R.string.message_size), - text = stringResource( - id = R.string.message_size_in_bytes, - ndefMessage.byteArrayLength - ) - ) - } - - WizardStepComponent( - icon = Icons.Default.Edit, - title = stringResource(id = R.string.discover_tag_title), - state = WizardStepState.CURRENT, - showVerticalDivider = false, - ) { - Column { - when (val e = nfcScanEvent) { - is Error -> { - // Show the error message. - ProgressItem( - text = stringResource(id = R.string.write_failed), - status = ProgressItemStatus.ERROR, - iconRightPadding = 24.dp, - ) - Text( - text = if (e.message.length > 35) e.message.slice(0..35) else e.message, - modifier = Modifier - .alpha(0.7f) - .padding(start = 48.dp), - style = MaterialTheme.typography.bodySmall, - ) - } - - Loading -> { - // Show the loading indicator. - ProgressItem( - text = stringResource(id = R.string.discovering_tag), - status = ProgressItemStatus.WORKING, - iconRightPadding = 24.dp, - ) - } + .padding(8.dp) + ) + } - Success -> { - ProgressItem( - text = stringResource(id = R.string.write_success), - status = ProgressItemStatus.SUCCESS, - iconRightPadding = 24.dp, - ) - Text( - text = stringResource(id = R.string.success_des), - modifier = Modifier - .alpha(0.7f) - .padding(start = 48.dp), - style = MaterialTheme.typography.bodySmall, - ) - } + // Show an option to search for a WiFi network. + OutlinedCardItem( + headline = stringResource(id = R.string.search_for_wifi_networks), + description = { + Text( + text = stringResource(id = R.string.search_for_wifi_networks_des), + style = MaterialTheme.typography.bodyMedium, + ) + }, + icon = Icons.Default.WifiFind, + onCardClick = { onEvent(OnScanClickEvent) } + ) { + Spacer(Modifier.weight(1f)) + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .clickable { onEvent(OnScanClickEvent) } + .padding(8.dp) + ) + } - null -> { - ProgressItem( - text = stringResource(id = R.string.tap_nfc_tag), - status = ProgressItemStatus.WORKING, - iconRightPadding = 24.dp, - ) - } - } - } - } - } + if (isDialogOpen) { + // Open a dialog to enter the WiFi credentials manually. + AddWifiManuallyDialog( + onCancelClick = { + isDialogOpen = false + }, + onConfirmClick = { + isDialogOpen = false + onEvent(OnAddWifiNetworkClickEvent(it)) + }, + ) } } } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index d03c945c..33057252 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -12,6 +12,7 @@ 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.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -31,10 +32,14 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState 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.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -78,107 +83,118 @@ internal fun WifiScannerScreen() { val viewState by wifiScannerViewModel.viewState.collectAsStateWithLifecycle() var isGroupedBySsid by rememberSaveable { mutableStateOf(false) } val groupIcon = if (isGroupedBySsid) Icons.Outlined.GroupRemove else Icons.Outlined.Group + val snackbarHostState = remember { SnackbarHostState() } // Handle the back press. BackHandler { onEvent(OnNavigateUpClickEvent) } // Show the scanning screen. - Column( - modifier = Modifier - .fillMaxSize() - .padding(bottom = 56.dp) - ) { - NordicAppBar( - text = stringResource(id = R.string.wifi_scanner_appbar), - showBackButton = true, - onNavigationButtonClick = { onEvent(OnNavigateUpClickEvent) }, - actions = { - // Show the group icon to group by SSID. - Icon( - imageVector = groupIcon, - contentDescription = null, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .clickable { - isGroupedBySsid = !isGroupedBySsid - } - .padding(8.dp) - - ) - } - ) + Scaffold( + contentWindowInsets = WindowInsets(0, 0, 0, 0), + topBar = { + NordicAppBar( + text = stringResource(id = R.string.wifi_scanner_appbar), + showBackButton = true, + onNavigationButtonClick = { onEvent(OnNavigateUpClickEvent) }, + actions = { + // Show the group icon to group by SSID. + Icon( + imageVector = groupIcon, + contentDescription = null, + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable { + isGroupedBySsid = !isGroupedBySsid + } + .padding(8.dp) - RequireWifi { - RequireLocationForWifi { - when (val scanningState = viewState.networks) { - is Error -> { - // Show the error message. - Column { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text(text = stringResource(id = R.string.error_while_scanning)) - Text(text = scanningState.t.message ?: "Unknown error occurred.") + ) + } + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + ) { + RequireWifi { + RequireLocationForWifi { + when (val scanningState = viewState.networks) { + is Error -> { + // Show the error message. + Column { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = stringResource(id = R.string.error_while_scanning)) + Text( + text = scanningState.t.message ?: "Unknown error occurred." + ) + } } } - } - is Loading -> { - // Show the loading indicator. - Column { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - modifier = Modifier.padding(16.dp) - ) + is Loading -> { + // Show the loading indicator. + Column { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.padding(16.dp) + ) + } } } - } - is Success -> { - // Show the list of available networks. - Column { - WifiSortView(viewState.sortOption) { - onEvent(OnSortOptionSelected(it)) - } - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (isGroupedBySsid) { - // Group by SSID - GroupBySsid(viewState.sortedItems, onEvent) - } else { - // Show the list of available networks grouped by SSIDs. - WifiList(viewState.sortedItems, onEvent) + is Success -> { + // Show the list of available networks. + Column { + WifiSortView(viewState.sortOption) { + onEvent(OnSortOptionSelected(it)) + } + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (isGroupedBySsid) { + // Group by SSID + GroupBySsid(viewState.sortedItems, onEvent) + } else { + // Show the list of available networks grouped by SSIDs. + WifiList(viewState.sortedItems, onEvent) + } } } } } } - } - when (val selectedNetwork = viewState.selectedNetwork) { - null -> { - // Do nothing - } + when (val selectedNetwork = viewState.selectedNetwork) { + null -> { + // Do nothing + } - else -> { - // Show the password dialog - PasswordDialog( - scanResult = selectedNetwork, - onCancelClick = { - // Dismiss the dialog - // Set the selected network to null - onEvent(OnPasswordCancelEvent) - }) { - onEvent(OnPasswordSetEvent(it)) + else -> { + // Show the password dialog + PasswordDialog( + scanResult = selectedNetwork, + onCancelClick = { + // Dismiss the dialog + // Set the selected network to null + onEvent(OnPasswordCancelEvent) + }) { + onEvent(OnPasswordSetEvent(it)) + } } } } @@ -287,7 +303,8 @@ private fun GroupBySsid( ) { networks.groupBy { it.SSID }.forEach { (ssid, network) -> var isExpanded by rememberSaveable { mutableStateOf(false) } - val expandIcon = if (isExpanded) Icons.Outlined.ExpandLess else Icons.Outlined.ExpandMore + val expandIcon = + if (isExpanded) Icons.Outlined.ExpandLess else Icons.Outlined.ExpandMore // Skip hidden networks. if (ssid == null || ssid.isEmpty()) { From a7ce3ec8f7738a39a2221d0f9b7304a0ab6a9a7b Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 4 Jun 2024 13:13:01 +0200 Subject: [PATCH 47/78] Fixed padding --- .../provisioner/feature/nfc/uicomponent/OutlinedCardItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/OutlinedCardItem.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/OutlinedCardItem.kt index efd6918a..55c67eea 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/OutlinedCardItem.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/OutlinedCardItem.kt @@ -51,7 +51,7 @@ fun OutlinedCardItem( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier - .padding(8.dp) + .padding(start= 16.dp, end = 8.dp, top = 8.dp, bottom = 8.dp) .fillMaxWidth() ) { Icon( From 66f5f8e20385c342a6e9614484b052534bbc7096 Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 4 Jun 2024 14:23:36 +0200 Subject: [PATCH 48/78] Removed RequireApi from MainActivity --- .../android/wifi/provisioner/HomeScreen.kt | 11 +++++++- .../android/wifi/provisioner/MainActivity.kt | 26 +++++++++++-------- app/src/main/res/values/strings.xml | 1 + 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt index 7ed3a21b..470591cf 100644 --- a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt +++ b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt @@ -154,7 +154,16 @@ fun HomeScreen() { sectionTitle = stringResource(R.string.provision_over_nfc), sectionRational = stringResource(R.string.provision_over_nfc_rationale) ) { - vm.navigateTo(NfcDestination) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + vm.navigateTo(NfcDestination) + } else { + scope.launch { + snackbarHostState.showSnackbar( + message = context.getString(R.string.error_nfc_not_supported), + actionLabel = context.getString(no.nordicsemi.android.wifi.provisioner.ui.R.string.dismiss) + ) + } + } } } Row( diff --git a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt index bfbe145c..9a2a4f07 100644 --- a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt +++ b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/MainActivity.kt @@ -37,7 +37,6 @@ import android.os.Bundle import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.ui.Modifier @@ -52,7 +51,6 @@ import no.nordicsemi.android.wifi.provisioner.softap.view.SoftApProvisionerDesti @AndroidEntryPoint class MainActivity : NordicActivity() { - @RequiresApi(Build.VERSION_CODES.M) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -63,15 +61,21 @@ class MainActivity : NordicActivity() { Surface(modifier = Modifier.fillMaxSize()) { NavigationView( destinations = (HomeDestination + - BleProvisioningDestinations + - NfcProvisionerDestinations).run { - // Soft AP is available on Android 10 and newer. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - this + SoftApProvisionerDestinations - } else { - this - } - } + BleProvisioningDestinations).run { + // Soft AP is available on Android 10 and newer. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + this + SoftApProvisionerDestinations + } else { + this + } + }.run { + // NFC is available on Android 6.0 and newer. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + this + NfcProvisionerDestinations + } else { + this + } + } ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ddc8fd9c..0b222e9d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,4 +49,5 @@ This mode allows provisioning a nRF700x device to a Wi-Fi network by providing the Wi-Fi credentials via NFC. + This feature requires an Android device running Android 6.0 or above. \ No newline at end of file From b811f4bdfdaca4e02ed21768cc61ee00bac3a0bd Mon Sep 17 00:00:00 2001 From: roshanrajaratnam Date: Tue, 4 Jun 2024 15:23:58 +0200 Subject: [PATCH 49/78] Fixes the home screen with correct padding --- .../android/wifi/provisioner/HomeScreen.kt | 218 ++++++++++++++---- .../ui/view/section/ProvisionSection.kt | 2 - 2 files changed, 167 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt index 70293b25..14cba95d 100644 --- a/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt +++ b/app/src/main/java/no/nordicsemi/android/wifi/provisioner/HomeScreen.kt @@ -31,11 +31,15 @@ package no.nordicsemi.android.wifi.provisioner +import android.content.Context +import android.content.res.Configuration import android.os.Build import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.consumeWindowInsets @@ -44,6 +48,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState @@ -59,13 +64,16 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import no.nordicsemi.android.common.navigation.DestinationId import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel import no.nordicsemi.android.common.theme.view.NordicAppBar import no.nordicsemi.android.wifi.provisioner.app.BuildConfig @@ -82,6 +90,10 @@ fun HomeScreen() { val context = LocalContext.current val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } + val isLargeScreen = + LocalConfiguration.current.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_LARGE + val isLandscape = + LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE Scaffold( contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = { @@ -93,54 +105,157 @@ fun HomeScreen() { SnackbarHost(hostState = snackbarHostState) }, ) { innerPadding -> + + when { + !isLargeScreen && isLandscape -> { + SmallScreenLandscapeContent( + context = context, + scope = scope, + snackbarHostState = snackbarHostState, + innerPadding = innerPadding, + navigateTo = vm::navigateTo + ) + } + + else -> { + PortraitContent( + context = context, + scope = scope, + snackbarHostState = snackbarHostState, + innerPadding = innerPadding, + navigateTo = vm::navigateTo + ) + } + } + } +} + +@Composable +private fun PortraitContent( + context: Context, + scope: CoroutineScope, + snackbarHostState: SnackbarHostState, + innerPadding: PaddingValues, + navigateTo: (DestinationId) -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(state = rememberScrollState()) + .padding(innerPadding) + .padding(bottom = 56.dp) + .consumeWindowInsets(innerPadding) + .windowInsetsPadding( + WindowInsets.safeDrawing.only( + WindowInsetsSides.Horizontal, + ), + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.ic_nrf70), + contentDescription = stringResource(id = R.string.ic_nrf70), + modifier = Modifier + .widthIn(max = 200.dp) + .weight(0.5f, fill = true) + .padding(8.dp) + ) Column( modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .verticalScroll(rememberScrollState()) - .consumeWindowInsets(innerPadding) - .windowInsetsPadding( - WindowInsets.safeDrawing.only( - WindowInsetsSides.Horizontal, - ), - ), - horizontalAlignment = Alignment.CenterHorizontally + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - Row( - modifier = Modifier - .fillMaxWidth() - .weight(1f, true), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = painterResource(id = R.drawable.ic_nrf70), - contentDescription = stringResource(id = R.string.ic_nrf70), - modifier = Modifier - .widthIn(max = 200.dp) - .padding(8.dp) - ) - } - Column( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - ProvisionSection( - sectionTitle = stringResource(R.string.provision_over_ble), - sectionRational = stringResource(R.string.provision_over_ble_rationale) - ) { - vm.navigateTo(BleDestination) + Spacer(modifier = Modifier.size(16.dp)) + ProvisionSection( + sectionTitle = stringResource(R.string.provision_over_ble), + sectionRational = stringResource(R.string.provision_over_ble_rationale), + onClick = { navigateTo(BleDestination) } + ) + Spacer(modifier = Modifier.size(16.dp)) + ProvisionSection( + sectionTitle = stringResource(R.string.provision_over_wifi), + sectionRational = stringResource(R.string.provision_over_wifi_rationale), + onClick = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + navigateTo(SoftApDestination) + } else { + scope.launch { + snackbarHostState.showSnackbar( + message = context.getString(R.string.error_softap_not_supported), + actionLabel = context.getString(no.nordicsemi.android.wifi.provisioner.ui.R.string.dismiss) + ) + } + } } + ) + Spacer(modifier = Modifier.size(16.dp)) + ProvisionSection( + sectionTitle = stringResource(R.string.provision_over_nfc), + sectionRational = stringResource(R.string.provision_over_nfc_rationale), + onClick = { navigateTo(NfcProvisionerDestinationId) } + ) + } + Text( + text = stringResource( + id = R.string.app_version, + BuildConfig.VERSION_NAME, + BuildConfig.VERSION_CODE + ), + textAlign = TextAlign.End, + style = MaterialTheme.typography.labelMedium + ) + } +} - ProvisionSection( - sectionTitle = stringResource(R.string.provision_over_wifi), - sectionRational = stringResource(R.string.provision_over_wifi_rationale) - ) { +@Composable +private fun SmallScreenLandscapeContent( + context: Context, + scope: CoroutineScope, + snackbarHostState: SnackbarHostState, + innerPadding: PaddingValues, + navigateTo: (DestinationId) -> Unit +) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 16.dp) + .consumeWindowInsets(innerPadding) + .windowInsetsPadding( + WindowInsets.safeDrawing.only( + WindowInsetsSides.Horizontal, + ), + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Column { + Image( + modifier = Modifier.padding(horizontal = 56.dp), + painter = painterResource(id = R.drawable.ic_nrf70), + contentDescription = stringResource(id = R.string.ic_nrf70), + ) + } + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(state = rememberScrollState()) + ) { + Spacer(modifier = Modifier.size(16.dp)) + ProvisionSection( + sectionTitle = stringResource(R.string.provision_over_ble), + sectionRational = stringResource(R.string.provision_over_ble_rationale), + onClick = { navigateTo(BleDestination) } + ) + Spacer(modifier = Modifier.size(16.dp)) + ProvisionSection( + sectionTitle = stringResource(R.string.provision_over_wifi), + sectionRational = stringResource(R.string.provision_over_wifi_rationale), + onClick = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - vm.navigateTo(SoftApDestination) + navigateTo(SoftApDestination) } else { scope.launch { snackbarHostState.showSnackbar( @@ -150,18 +265,19 @@ fun HomeScreen() { } } } - ProvisionSection( - sectionTitle = stringResource(R.string.provision_over_nfc), - sectionRational = stringResource(R.string.provision_over_nfc_rationale) - ) { - vm.navigateTo(NfcProvisionerDestinationId) - } - } + ) + Spacer(modifier = Modifier.size(16.dp)) + ProvisionSection( + sectionTitle = stringResource(R.string.provision_over_nfc), + sectionRational = stringResource(R.string.provision_over_nfc_rationale), + onClick = { navigateTo(NfcProvisionerDestinationId) } + ) + Spacer(modifier = Modifier.size(16.dp)) Row( modifier = Modifier .fillMaxWidth() - .padding(bottom = 16.dp), - horizontalArrangement = Arrangement.Center, + .padding(end = 8.dp), + horizontalArrangement = Arrangement.End, ) { Text( text = stringResource( diff --git a/feature/ui/src/main/java/no/nordicsemi/android/wifi/provisioner/ui/view/section/ProvisionSection.kt b/feature/ui/src/main/java/no/nordicsemi/android/wifi/provisioner/ui/view/section/ProvisionSection.kt index e27da706..6d161830 100644 --- a/feature/ui/src/main/java/no/nordicsemi/android/wifi/provisioner/ui/view/section/ProvisionSection.kt +++ b/feature/ui/src/main/java/no/nordicsemi/android/wifi/provisioner/ui/view/section/ProvisionSection.kt @@ -32,8 +32,6 @@ fun ProvisionSection( OutlinedCard( modifier = Modifier .widthIn(max = 600.dp) - .fillMaxWidth() - .padding(8.dp) .clickable { onClick() } ) { Column(modifier = Modifier.padding(16.dp)) { From 18408da9db6cedcd82a77367d2c6245d956b8bf3 Mon Sep 17 00:00:00 2001 From: roshanrajaratnam Date: Tue, 4 Jun 2024 15:24:18 +0200 Subject: [PATCH 50/78] Fixes the home screen with correct padding --- .../android/wifi/provisioner/ui/view/section/ProvisionSection.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/feature/ui/src/main/java/no/nordicsemi/android/wifi/provisioner/ui/view/section/ProvisionSection.kt b/feature/ui/src/main/java/no/nordicsemi/android/wifi/provisioner/ui/view/section/ProvisionSection.kt index 6d161830..a5d4bacd 100644 --- a/feature/ui/src/main/java/no/nordicsemi/android/wifi/provisioner/ui/view/section/ProvisionSection.kt +++ b/feature/ui/src/main/java/no/nordicsemi/android/wifi/provisioner/ui/view/section/ProvisionSection.kt @@ -3,7 +3,6 @@ package no.nordicsemi.android.wifi.provisioner.ui.view.section import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn From e59ccf398c282a65a7095487fc4ff7846f9213fa Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 4 Jun 2024 15:26:47 +0200 Subject: [PATCH 51/78] Removed hilt injection from lib --- .../provisioner/feature}/nfc/di/NfcAdapter.kt | 2 +- .../nfc/di/WifiManagerRepositoryModule.kt | 16 +++-- lib/nfc/build.gradle.kts | 2 - .../provisioner/nfc/NdefMessageBuilder.kt | 3 +- .../wifi/provisioner/nfc/NfcManagerForWifi.kt | 5 +- .../provisioner/nfc/WifiManagerRepository.kt | 54 ++++++++++++++-- .../nfc/WifiManagerRepositoryImp.kt | 64 ------------------- 7 files changed, 60 insertions(+), 86 deletions(-) rename {lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner => feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature}/nfc/di/NfcAdapter.kt (92%) rename {lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner => feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature}/nfc/di/WifiManagerRepositoryModule.kt (52%) delete mode 100644 lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/di/NfcAdapter.kt similarity index 92% rename from lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/di/NfcAdapter.kt index 7f3ac218..efc3a369 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/NfcAdapter.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/di/NfcAdapter.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.wifi.provisioner.nfc.di +package no.nordicsemi.android.wifi.provisioner.feature.nfc.di import android.content.Context import android.nfc.NfcAdapter diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/di/WifiManagerRepositoryModule.kt similarity index 52% rename from lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/di/WifiManagerRepositoryModule.kt index 375c0f86..8fda0c34 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/di/WifiManagerRepositoryModule.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/di/WifiManagerRepositoryModule.kt @@ -1,24 +1,26 @@ -package no.nordicsemi.android.wifi.provisioner.nfc.di +package no.nordicsemi.android.wifi.provisioner.feature.nfc.di import android.content.Context -import android.os.Build -import androidx.annotation.RequiresApi import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import no.nordicsemi.android.wifi.provisioner.nfc.NdefMessageBuilder import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository -import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepositoryImp import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object WifiManagerRepositoryModule { - @RequiresApi(Build.VERSION_CODES.M) @Provides @Singleton - fun provideWifiManagerRepositoryModule(@ApplicationContext context: Context): WifiManagerRepository = - WifiManagerRepositoryImp(context = context) + fun provideNdefMessageBuilder() = NdefMessageBuilder() + + @Provides + @Singleton + fun provideWifiManagerRepository( + @ApplicationContext context: Context + ) = WifiManagerRepository(context) } \ No newline at end of file diff --git a/lib/nfc/build.gradle.kts b/lib/nfc/build.gradle.kts index 14653f6c..3a619667 100644 --- a/lib/nfc/build.gradle.kts +++ b/lib/nfc/build.gradle.kts @@ -1,7 +1,6 @@ plugins { alias(libs.plugins.nordic.library) alias(libs.plugins.nordic.kotlin) - alias(libs.plugins.nordic.hilt) alias(libs.plugins.kotlin.parcelize) } @@ -11,5 +10,4 @@ android { dependencies { implementation(libs.nordic.ble.ktx) - implementation(libs.nordic.ble.common) } \ No newline at end of file diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt index 491a387c..27e13d90 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt @@ -31,12 +31,11 @@ import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WP import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WPA_WPA2_PSK import java.nio.ByteBuffer import java.nio.charset.Charset -import javax.inject.Inject /** * This class is responsible for creating the NDEF message for the WiFi data. */ -class NdefMessageBuilder @Inject constructor() { +class NdefMessageBuilder { /** * Creates the NDEF message for the WiFi data. diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt index 1be604b3..70dfed82 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NfcManagerForWifi.kt @@ -8,8 +8,6 @@ import android.nfc.tech.Ndef import android.nfc.tech.NdefFormatable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import javax.inject.Inject -import javax.inject.Singleton enum class NfcFlags(val value: Int) { NFC_A(NfcAdapter.FLAG_READER_NFC_A), @@ -36,8 +34,7 @@ data class Error(val message: String) : NfcScanEvent /** * A class that manages the NFC adapter for the wifi provisioning. */ -@Singleton -class NfcManagerForWifi @Inject constructor( +class NfcManagerForWifi( private val nfcAdapter: NfcAdapter?, ) { private val _nfcScanEvent = MutableStateFlow(null) diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt index c677ba1e..c85b2395 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt @@ -1,21 +1,63 @@ package no.nordicsemi.android.wifi.provisioner.nfc +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.net.wifi.ScanResult -import kotlinx.coroutines.flow.StateFlow +import android.net.wifi.WifiManager +import android.os.Build +import androidx.annotation.RequiresApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState +import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success /** - * A repository interface to manage the wifi manager. + * A repository class to manage the Wi-Fi scan. */ -sealed interface WifiManagerRepository { +class WifiManagerRepository( + context: Context, +) { + private var wifiManager: WifiManager = + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + private val _networkState = MutableStateFlow>>(Loading()) + val networkState = _networkState.asStateFlow() /** - * A state flow to represent the network state of the wifi scan. + * Broadcast receiver to receive the scan results. */ - val networkState: StateFlow>> + private val wifiScanReceiver = object : BroadcastReceiver() { + @RequiresApi(Build.VERSION_CODES.M) + override fun onReceive(context: Context, intent: Intent) { + try { + val success = + intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false) + if (!success) { + wifiManager.startScan() + } + val results = wifiManager.scanResults + _networkState.value = Success(results) + } catch (e: Exception) { + e.printStackTrace() + _networkState.value = Error(e) + } + } + } + + init { + // Register the broadcast receiver + val intentFilter = IntentFilter() + intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) + context.registerReceiver(wifiScanReceiver, intentFilter) + } /** * This method is used to start the wifi scan. */ - fun onScan() + fun onScan() { + wifiManager.startScan() + } } \ No newline at end of file diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt deleted file mode 100644 index b2f89c36..00000000 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepositoryImp.kt +++ /dev/null @@ -1,64 +0,0 @@ -package no.nordicsemi.android.wifi.provisioner.nfc - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.net.wifi.ScanResult -import android.net.wifi.WifiManager -import android.os.Build -import androidx.annotation.RequiresApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error -import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading -import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState -import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success -import javax.inject.Singleton - -@RequiresApi(Build.VERSION_CODES.M) -@SuppressWarnings("MissingPermission") -@Singleton -internal class WifiManagerRepositoryImp( - context: Context, -) : WifiManagerRepository { - private var wifiManager: WifiManager = - context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager - private val _networkState = MutableStateFlow>>(Loading()) - override val networkState = _networkState.asStateFlow() - - /** - * Broadcast receiver to receive the scan results. - */ - private val wifiScanReceiver = object : BroadcastReceiver() { - @RequiresApi(Build.VERSION_CODES.M) - override fun onReceive(context: Context, intent: Intent) { - try { - val success = - intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false) - if (!success) { - wifiManager.startScan() - } - val results = wifiManager.scanResults - _networkState.value = Success(results) - } catch (e: Exception) { - e.printStackTrace() - _networkState.value = Error(e) - } - } - } - - init { - // Register the broadcast receiver - val intentFilter = IntentFilter() - intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) - context.registerReceiver(wifiScanReceiver, intentFilter) - } - - /** - * This method is used to start the wifi scan. - */ - override fun onScan() { - wifiManager.startScan() - } -} From 37bca99f55b663b707da231a944838543ba7c505 Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 4 Jun 2024 15:40:37 +0200 Subject: [PATCH 52/78] Require location permission to scan wifi networks --- .../android/wifi/provisioner/nfc/WifiManagerRepository.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt index c85b2395..434ff05a 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt @@ -8,6 +8,7 @@ import android.net.wifi.ScanResult import android.net.wifi.WifiManager import android.os.Build import androidx.annotation.RequiresApi +import androidx.annotation.RequiresPermission import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error @@ -31,6 +32,7 @@ class WifiManagerRepository( */ private val wifiScanReceiver = object : BroadcastReceiver() { @RequiresApi(Build.VERSION_CODES.M) + @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) override fun onReceive(context: Context, intent: Intent) { try { val success = From 28a4045fef410ad4f10d2ccbebcebefc722f28c9 Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 4 Jun 2024 16:09:35 +0200 Subject: [PATCH 53/78] Don't ask password if network is open --- .../nfc/uicomponent/AddWifiManuallyDialog.kt | 44 ++++++++++++------- .../feature/nfc/uicomponent/PasswordDialog.kt | 6 +-- feature/nfc/src/main/res/values/strings.xml | 1 + 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt index 2bda5d90..ae7f1d3b 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt @@ -41,9 +41,9 @@ internal fun AddWifiManuallyDialog( var ssid by rememberSaveable { mutableStateOf("") } var password by rememberSaveable { mutableStateOf("") } var showPassword by rememberSaveable { mutableStateOf(false) } + var isPasswordEmpty by rememberSaveable { mutableStateOf(false) } var authMode by rememberSaveable { mutableStateOf("WPA2-Personal") } // default to WPA2-Personal. var encryptionMode by rememberSaveable { mutableStateOf(EncryptionMode.AES.toString()) } // default to AES. - var isSsidEmpty by rememberSaveable { mutableStateOf(false) } AlertDialog( @@ -86,23 +86,30 @@ internal fun AddWifiManuallyDialog( input = ssid, label = stringResource(id = R.string.ssid_label), placeholder = stringResource(id = R.string.ssid_placeholder), - errorState = isSsidEmpty && ssid.isEmpty(), + errorState = isSsidEmpty && ssid.trim().isEmpty(), errorMessage = stringResource(id = R.string.ssid_error), onUpdate = { ssid = it isSsidEmpty = ssid.isEmpty() } ) - - // Show the password field. - PasswordInputField( - input = password, - label = stringResource(id = R.string.password), - placeholder = stringResource(id = R.string.password_placeholder), - showPassword = showPassword, - onShowPassChange = { showPassword = !showPassword }, - onUpdate = { password = it }, - ) + // Show the password field only if the authentication mode is not open. + if (authMode.lowercase() != "open") { + // Show the password field. + PasswordInputField( + input = password, + label = stringResource(id = R.string.password), + placeholder = stringResource(id = R.string.password_placeholder), + isError = isPasswordEmpty && password.trim().isEmpty(), + errorMessage = stringResource(id = R.string.password_error), + showPassword = showPassword, + onShowPassChange = { showPassword = !showPassword }, + onUpdate = { password = it }, + ) + } else { + // Clear the password if the authentication mode is open. + password = "" + } } }, dismissButton = { @@ -115,11 +122,14 @@ internal fun AddWifiManuallyDialog( confirmButton = { TextButton( onClick = { - if (ssid.isEmpty()) { - isSsidEmpty = true - return@TextButton - } else { - onConfirmClick( + // Validate the fields. + when { + ssid.trim().isEmpty() -> isSsidEmpty = true + + authMode.lowercase() != "open" && password.trim() + .isEmpty() -> isPasswordEmpty = true + + else -> onConfirmClick( WifiData( ssid = ssid, password = password, diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt index 543a7887..d3b7bfeb 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt @@ -76,12 +76,12 @@ internal fun PasswordDialog( label = stringResource(id = R.string.password), placeholder = stringResource(id = R.string.password_placeholder), showPassword = showPassword, - isError = isPasswordEmpty && password.isEmpty(), - errorMessage = "Password cannot be empty.", + isError = isPasswordEmpty && password.trim().isEmpty(), + errorMessage = stringResource(id = R.string.password_error), onShowPassChange = { showPassword = !showPassword }, onUpdate = { password = it - isPasswordEmpty = password.isEmpty() + isPasswordEmpty = password.trim().isEmpty() }, ) } diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index 3c646803..4f772c39 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -38,6 +38,7 @@ Select encryption Password Enter password + Password is required Wi-Fi record From c485668a7ec4f1525f56e2d2f8e47e7713eca65c Mon Sep 17 00:00:00 2001 From: hiar Date: Tue, 4 Jun 2024 16:54:09 +0200 Subject: [PATCH 54/78] Don't ask password if network is open --- .../nfc/viewmodel/WifiScannerViewModel.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index 0d8c147a..c952c3fb 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -12,7 +12,10 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcPublishDestination +import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.AuthMode +import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.WifiAuthType import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository +import no.nordicsemi.android.wifi.provisioner.nfc.domain.EncryptionMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success @@ -105,7 +108,21 @@ internal class WifiScannerViewModel @Inject constructor( fun onEvent(event: WifiScannerViewEvent) { when (event) { is OnNetworkSelectEvent -> { - _viewState.value = _viewState.value.copy(selectedNetwork = event.network) + val isProtected = WifiAuthType.getSecurityTypes(event.network) != AuthMode.OPEN.toString() + // If the network is open, navigate to the NFC screen + if (!isProtected) { + navigateToNfcScan( + WifiData( + ssid = event.network.SSID, + password = "", // Empty password for open network. + authType = AuthMode.OPEN.toString(), + encryptionMode = EncryptionMode.NONE.toString() // No encryption for open network. + ) + ) + } else { + // Show the dialog to enter the password for the selected network. + _viewState.value = _viewState.value.copy(selectedNetwork = event.network) + } } OnNavigateUpClickEvent -> onBackClick() From 2d3c43a60c83b8d0e7f8d6b8baa36bbf1dc6955e Mon Sep 17 00:00:00 2001 From: hiar Date: Wed, 5 Jun 2024 09:23:23 +0200 Subject: [PATCH 55/78] Excluded scanner destination on back navigation --- .../feature/nfc/viewmodel/WifiScannerViewModel.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index c952c3fb..2989289b 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcPublishDestination +import no.nordicsemi.android.wifi.provisioner.feature.nfc.WifiScannerDestination import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.AuthMode import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.WifiAuthType import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository @@ -148,8 +149,12 @@ internal class WifiScannerViewModel @Inject constructor( */ private fun navigateToNfcScan(wifiData: WifiData) { navigator.navigateTo( - NfcPublishDestination, - wifiData - ) + to = NfcPublishDestination, + args = wifiData + ) { + popUpTo(WifiScannerDestination.toString()) { + inclusive = true + } + } } } From ba6a40d77bdddc739caa269770548ad64ec617a6 Mon Sep 17 00:00:00 2001 From: hiar Date: Wed, 5 Jun 2024 16:28:14 +0200 Subject: [PATCH 56/78] Improved auth mode --- .../feature/nfc/data/WifiAuthType.kt | 135 ------------------ .../feature/nfc/mapping/UiMapper.kt | 53 +++++++ .../nfc/uicomponent/AddWifiManuallyDialog.kt | 7 +- .../feature/nfc/uicomponent/PasswordDialog.kt | 6 +- .../feature/nfc/view/NfcPublishScreen.kt | 11 +- .../feature/nfc/view/WifiScannerView.kt | 13 +- .../nfc/viewmodel/WifiScannerViewModel.kt | 13 +- .../provisioner/nfc/NdefMessageBuilder.kt | 30 ++-- .../nfc/domain/AuthenticationMode.kt | 107 ++++++++++++++ .../wifi/provisioner/nfc/domain/WifiData.kt | 2 +- .../nfc/domain/WifiHandoverDataType.kt | 8 -- 11 files changed, 199 insertions(+), 186 deletions(-) delete mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/mapping/UiMapper.kt create mode 100644 lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/AuthenticationMode.kt diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt deleted file mode 100644 index f8b7e882..00000000 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/data/WifiAuthType.kt +++ /dev/null @@ -1,135 +0,0 @@ -package no.nordicsemi.android.wifi.provisioner.feature.nfc.data - -import android.net.wifi.ScanResult -import android.os.Build - -object WifiAuthType { - /** - * Constants for security types. - * - * Note: This is for Build version sdk S and below. - */ - private const val OPEN = "Open" - private const val WEP = "WEP" - private const val WPA_PSK = "WPA-PSK" - private const val WPA2_PSK = "WPA2-PSK" - private const val WPA_WPA2_PSK = "WPA/WPA2-PSK" - private const val WPA2_EAP = "WPA2-EAP" - private const val WPA3_PSK = "WPA3-PSK" - - private val securityTypes = - listOf(OPEN, WEP, WPA_PSK, WPA2_PSK, WPA_WPA2_PSK, WPA2_EAP, WPA3_PSK) - - /** - * @return The security of a given [ScanResult]. - */ - fun getSecurityTypes(scanResult: ScanResult): String { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return AuthMode.getMatchedAuthMode(scanResult.securityTypes) - } else { - val cap = scanResult.capabilities - val securityModes = securityTypes - for (i in securityModes.indices.reversed()) { - if (cap.contains(securityModes[i])) { - return mapToUi(securityModes[i]) - } - } - return mapToUi(AuthMode.OPEN.name) - } - } - - /** - * Maps the security type to a user friendly string. - * - * Note: This is for Build version sdk S and below. - */ - private fun mapToUi(mode: String): String { - return when (mode) { - "WPA-PSK", "WPA_PSK" -> "WPA-Personal" - "WPA2-PSK", "WPA2_PSK" -> "WPA2-Personal" - "WPA/WPA2-PSK", "WPA_WPA2_PSK" -> "WPA/WPA2-Personal" - "WPA2-EAP", "WPA2_EAP" -> "WPA2-Enterprise" - "WPA3-PSK", "WPA3_PSK" -> "WPA3-Personal" - "WEP" -> "Shared" - else -> "Open" - } - } - - /** - * @return The list of security types. - */ - fun authList(): List { - return securityTypes.map { mapToUi(it) } - } -} - -/** - * Enum class that represents the authentication mode of a wifi network. - * - * Note: This is for Build version sdk Tiramisu (API 31) and above. - */ -enum class AuthMode(val id: Int) { - UNKNOWN(-1), - OPEN(0), - WEP(1), // Shared - WPA_PSK(2), - WPA2_EAP(3), - WPA3_PSK(4), // WPA3-Personal (SAE) password) - EAP_WPA3_ENTERPRISE_192_BIT(5), - OWE(6), // Opportunistic Wireless Encryption - WAPI_PSK(7), // WAPI pre-shared key (PSK) - WAPI_CERT(8), // WAPI certificate to be specified. - EAP_WPA3_ENTERPRISE(9), - OSEN(10), // Hotspot 2. - PASSPOINT_R1_R2(11), - PASSPOINT_R3(12), - DPP(13); // Security type for Easy Connect (DPP) network - - override fun toString(): String { - return when (this) { - UNKNOWN -> "Unknown" - OPEN -> "Open" - WEP -> "WEP" - WPA_PSK -> "WPA-Personal" - WPA2_EAP -> "WPA2-Enterprise" - WPA3_PSK -> "WPA3-Personal" - EAP_WPA3_ENTERPRISE_192_BIT -> "EAP-WPA3-ENTERPRISE-192-BIT" - OWE -> "OWE" - WAPI_PSK -> "WAPI-Personal" - WAPI_CERT -> "WAPI-CERT" - EAP_WPA3_ENTERPRISE -> "EAP-WPA3-ENTERPRISE" - OSEN -> "OSEN" - PASSPOINT_R1_R2 -> "PASSPOINT-R1-R2" - PASSPOINT_R3 -> "PASSPOINT-R3" - DPP -> "DPP" - } - } - - companion object { - - /** - * Get the authentication mode of a given security type. - * @param securityTypes The security type of the wifi network. - * @return The authentication mode of the wifi network. - */ - fun getMatchedAuthMode(securityTypes: IntArray): String { - val matchedType = mutableListOf() - securityTypes.forEach { type -> - val authMode = AuthMode.entries.find { it.id == type } - if (authMode != null) { - matchedType.add(authModeTOString(authMode)) - } - } - return matchedType.joinToString(", ") - } - - /** - * Maps the authentication mode to a user friendly string. - * - * Note: This is for Build version sdk Tiramisu (API 31) and above. - */ - private fun authModeTOString(authMode: AuthMode): String { - return authMode.toString() - } - } -} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/mapping/UiMapper.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/mapping/UiMapper.kt new file mode 100644 index 00000000..feea296c --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/mapping/UiMapper.kt @@ -0,0 +1,53 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.mapping + +import no.nordicsemi.android.wifi.provisioner.nfc.domain.AuthenticationMode +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeBelowTiramisu +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeTiramisuOrAbove + +/** + * Converts the [AuthenticationMode] to a display string. + * + * @return The display string. + */ +fun AuthenticationMode.toDisplayString(): String = when (this) { + WifiAuthTypeBelowTiramisu.OPEN, WifiAuthTypeTiramisuOrAbove.OPEN -> "Open" + WifiAuthTypeBelowTiramisu.WEP, WifiAuthTypeTiramisuOrAbove.WEP -> "Shared" + WifiAuthTypeBelowTiramisu.WPA_PSK, WifiAuthTypeTiramisuOrAbove.WPA_PSK -> "WPA-Personal" + WifiAuthTypeBelowTiramisu.WPA2_PSK -> "WPA2-Personal" + WifiAuthTypeBelowTiramisu.WPA_WPA2_PSK -> "WPA/WPA2-Personal" + WifiAuthTypeBelowTiramisu.WPA2_EAP, WifiAuthTypeTiramisuOrAbove.WPA2_EAP -> "WPA2-Enterprise" + WifiAuthTypeBelowTiramisu.WPA3_PSK, WifiAuthTypeTiramisuOrAbove.WPA3_PSK -> "WPA3-Personal" + WifiAuthTypeTiramisuOrAbove.UNKNOWN -> "Unknown" + WifiAuthTypeTiramisuOrAbove.EAP_WPA3_ENTERPRISE_192_BIT -> "EAP-WPA3-Enterprise-192-Bit" + WifiAuthTypeTiramisuOrAbove.OWE -> "Opportunistic-Wireless-Encryption" + WifiAuthTypeTiramisuOrAbove.WAPI_PSK -> "WAPI-PSK" + WifiAuthTypeTiramisuOrAbove.WAPI_CERT -> "WAPI-Certificate" + WifiAuthTypeTiramisuOrAbove.EAP_WPA3_ENTERPRISE -> "EAP-WPA3-Enterprise" + WifiAuthTypeTiramisuOrAbove.OSEN -> "Hotspot-2" + WifiAuthTypeTiramisuOrAbove.PASSPOINT_R1_R2 -> "Passpoint-R1-R2" + WifiAuthTypeTiramisuOrAbove.PASSPOINT_R3 -> "Passpoint-R3" + WifiAuthTypeTiramisuOrAbove.DPP -> "DPP" + WifiAuthTypeBelowTiramisu.WPA_EAP -> "WPA-Enterprise" +} + +/** + * @return The list of security types supported to display in the dropdown. + */ +fun authListToDisplay(): List { + return WifiAuthTypeBelowTiramisu.entries.map { it.toDisplayString() } +} + +/** + * Converts the display string to [AuthenticationMode]. + * + * @return The [AuthenticationMode]. + */ +fun String.toAuthenticationMode(): AuthenticationMode = when (this) { + "Shared" -> WifiAuthTypeBelowTiramisu.WEP + "WPA-Personal" -> WifiAuthTypeBelowTiramisu.WPA_PSK + "WPA2-Personal" -> WifiAuthTypeBelowTiramisu.WPA2_PSK + "WPA/WPA2-Personal" -> WifiAuthTypeBelowTiramisu.WPA_WPA2_PSK + "WPA2-Enterprise" -> WifiAuthTypeBelowTiramisu.WPA2_EAP + "WPA3-Personal" -> WifiAuthTypeBelowTiramisu.WPA3_PSK + else -> WifiAuthTypeBelowTiramisu.OPEN +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt index ae7f1d3b..3e491985 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt @@ -23,7 +23,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import no.nordicsemi.android.wifi.provisioner.feature.nfc.R -import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.WifiAuthType +import no.nordicsemi.android.wifi.provisioner.feature.nfc.mapping.authListToDisplay +import no.nordicsemi.android.wifi.provisioner.feature.nfc.mapping.toAuthenticationMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.EncryptionMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData @@ -64,7 +65,7 @@ internal fun AddWifiManuallyDialog( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.verticalScroll(rememberScrollState()) ) { - val items = WifiAuthType.authList() + val items = authListToDisplay() // Show the authentication dropdown. DropdownView( items = items, @@ -133,7 +134,7 @@ internal fun AddWifiManuallyDialog( WifiData( ssid = ssid, password = password, - authType = authMode, + authType = authMode.toAuthenticationMode(), encryptionMode = encryptionMode, ) ) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt index d3b7bfeb..dd737933 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import no.nordicsemi.android.wifi.provisioner.feature.nfc.R -import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.WifiAuthType +import no.nordicsemi.android.wifi.provisioner.nfc.domain.AuthenticationMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.EncryptionMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData @@ -41,7 +41,7 @@ internal fun PasswordDialog( ) { var password by rememberSaveable { mutableStateOf("") } var isPasswordEmpty by rememberSaveable { mutableStateOf(false) } - val authMode = WifiAuthType.getSecurityTypes(scanResult) + val authMode = AuthenticationMode.get(scanResult) val encryptionMode = EncryptionMode.getEncryption(scanResult) AlertDialog( @@ -103,7 +103,7 @@ internal fun PasswordDialog( WifiData( ssid = scanResult.SSID, password = password, - authType = authMode, + authType = authMode.first(), encryptionMode = encryptionMode ) ) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt index 728b0689..0e73147c 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt @@ -36,6 +36,7 @@ import no.nordicsemi.android.common.theme.view.ProgressItemStatus import no.nordicsemi.android.common.theme.view.WizardStepComponent import no.nordicsemi.android.common.theme.view.WizardStepState import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.feature.nfc.mapping.toDisplayString import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.NfcPasswordRow import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.NfcTextRow import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.NfcManagerViewModel @@ -102,12 +103,10 @@ internal fun NfcPublishScreen() { if (wifiData.password.isNotEmpty()) { NfcPasswordRow(title = stringResource(id = R.string.password_title)) } - if (wifiData.authType.isNotEmpty()) { - NfcTextRow( - title = stringResource(id = R.string.authentication_title), - text = wifiData.authType - ) - } + NfcTextRow( + title = stringResource(id = R.string.authentication_title), + text = wifiData.authType.toDisplayString() + ) if (wifiData.encryptionMode.isNotEmpty()) { NfcTextRow( title = stringResource(id = R.string.encryption_title), diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index 33057252..90736216 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -52,8 +52,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import no.nordicsemi.android.common.theme.view.NordicAppBar import no.nordicsemi.android.wifi.provisioner.feature.nfc.R -import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.AuthMode -import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.WifiAuthType +import no.nordicsemi.android.wifi.provisioner.feature.nfc.mapping.toDisplayString import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireLocationForWifi import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireWifi import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.PasswordDialog @@ -65,9 +64,11 @@ import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnPasswordSe import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnSortOptionSelected import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewModel +import no.nordicsemi.android.wifi.provisioner.nfc.domain.AuthenticationMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeBelowTiramisu import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData import no.nordicsemi.android.wifi.provisioner.ui.view.WifiSortView @@ -229,8 +230,8 @@ private fun NetworkItem( modifier: Modifier = Modifier, onEvent: (WifiScannerViewEvent) -> Unit, ) { - val securityType = WifiAuthType.getSecurityTypes(network) - val isProtected = securityType != AuthMode.OPEN.name + val securityType = AuthenticationMode.get(network) + val isProtected = securityType.contains(WifiAuthTypeBelowTiramisu.OPEN).not() Row( verticalAlignment = Alignment.CenterVertically, @@ -246,7 +247,7 @@ private fun NetworkItem( val wifiData = WifiData( ssid = network.SSID, password = "", // Empty password for open networks - authType = AuthMode.OPEN.name, + authType = WifiAuthTypeBelowTiramisu.OPEN, ) onEvent(OnPasswordSetEvent(wifiData)) } @@ -277,7 +278,7 @@ private fun NetworkItem( ) // Display the security type of the access point. Text( - text = securityType, + text = securityType.joinToString(", ") { it.toDisplayString() }, modifier = Modifier.alpha(0.7f) ) } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index 2989289b..f5bed605 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -13,13 +13,14 @@ import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcPublishDestination import no.nordicsemi.android.wifi.provisioner.feature.nfc.WifiScannerDestination -import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.AuthMode -import no.nordicsemi.android.wifi.provisioner.feature.nfc.data.WifiAuthType import no.nordicsemi.android.wifi.provisioner.nfc.WifiManagerRepository +import no.nordicsemi.android.wifi.provisioner.nfc.domain.AuthenticationMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.EncryptionMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import no.nordicsemi.android.wifi.provisioner.nfc.domain.NetworkState import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeBelowTiramisu +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeTiramisuOrAbove import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.WifiSortOption import javax.inject.Inject @@ -109,14 +110,16 @@ internal class WifiScannerViewModel @Inject constructor( fun onEvent(event: WifiScannerViewEvent) { when (event) { is OnNetworkSelectEvent -> { - val isProtected = WifiAuthType.getSecurityTypes(event.network) != AuthMode.OPEN.toString() + val isOpen = AuthenticationMode.get(event.network) + .contains(WifiAuthTypeBelowTiramisu.OPEN) or AuthenticationMode.get(event.network) + .contains(WifiAuthTypeTiramisuOrAbove.OPEN) // If the network is open, navigate to the NFC screen - if (!isProtected) { + if (isOpen) { navigateToNfcScan( WifiData( ssid = event.network.SSID, password = "", // Empty password for open network. - authType = AuthMode.OPEN.toString(), + authType = WifiAuthTypeBelowTiramisu.OPEN, encryptionMode = EncryptionMode.NONE.toString() // No encryption for open network. ) ) diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt index 27e13d90..7dea3511 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt @@ -2,7 +2,10 @@ package no.nordicsemi.android.wifi.provisioner.nfc import android.nfc.NdefMessage import android.nfc.NdefRecord +import no.nordicsemi.android.wifi.provisioner.nfc.domain.AuthenticationMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.EncryptionMode +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeBelowTiramisu +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeTiramisuOrAbove import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_FIELD_ID import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_OPEN @@ -23,12 +26,6 @@ import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.MA import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.NETWORK_KEY_FIELD_ID import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.NFC_TOKEN_MIME_TYPE import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.SSID_FIELD_ID -import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WEP -import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WPA2_ENTERPRISE -import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WPA2_PSK -import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WPA_EAP -import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WPA_PSK -import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.WPA_WPA2_PSK import java.nio.ByteBuffer import java.nio.charset.Charset @@ -141,19 +138,14 @@ class NdefMessageBuilder { * * @param auth the authentication type. */ - private fun getAuthBytes(auth: String): Short { - // If Empty, default to WPA2-Personal - if (auth.isEmpty()) { - return AUTH_TYPE_WPA2_PSK - } - - return when { - auth.startsWith(WPA_PSK, ignoreCase = true) -> AUTH_TYPE_WPA_PSK - auth.startsWith(WPA_EAP, ignoreCase = true) -> AUTH_TYPE_WPA_EAP - auth.startsWith(WPA2_ENTERPRISE, ignoreCase = true) -> AUTH_TYPE_WPA2_EAP - auth.startsWith(WPA2_PSK, ignoreCase = true) -> AUTH_TYPE_WPA2_PSK - auth.startsWith(WPA_WPA2_PSK, ignoreCase = true) -> AUTH_TYPE_WPA_WPA2_PSK - auth.startsWith(WEP, ignoreCase = true) -> AUTH_TYPE_SHARED + private fun getAuthBytes(auth: AuthenticationMode): Short { + return when (auth) { + WifiAuthTypeBelowTiramisu.WEP, WifiAuthTypeTiramisuOrAbove.WEP -> AUTH_TYPE_SHARED + WifiAuthTypeBelowTiramisu.WPA_PSK, WifiAuthTypeTiramisuOrAbove.WPA_PSK -> AUTH_TYPE_WPA_PSK + WifiAuthTypeBelowTiramisu.WPA_EAP -> AUTH_TYPE_WPA_EAP + WifiAuthTypeBelowTiramisu.WPA2_PSK -> AUTH_TYPE_WPA2_PSK + WifiAuthTypeBelowTiramisu.WPA_WPA2_PSK -> AUTH_TYPE_WPA_WPA2_PSK + WifiAuthTypeBelowTiramisu.WPA2_EAP, WifiAuthTypeTiramisuOrAbove.WPA2_EAP -> AUTH_TYPE_WPA2_EAP else -> AUTH_TYPE_OPEN } } diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/AuthenticationMode.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/AuthenticationMode.kt new file mode 100644 index 00000000..598be336 --- /dev/null +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/AuthenticationMode.kt @@ -0,0 +1,107 @@ +package no.nordicsemi.android.wifi.provisioner.nfc.domain + +import android.net.wifi.ScanResult +import android.os.Build +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Interface that represents the authentication mode of a wifi network. + */ +@Parcelize +sealed interface AuthenticationMode : Parcelable { + + companion object { + + fun get(scanResult: ScanResult): List { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { + WifiAuthTypeTiramisuOrAbove.getMatchedAuthMode(scanResult.securityTypes) + } + + else -> { + listOf(WifiAuthTypeBelowTiramisu.getSecurityTypes(scanResult)) + } + } + } + } +} + +/** + * Enum class that represents the authentication mode of a wifi network. + * + * Note: This is for Build version sdk below Tiramisu. + */ +@Parcelize +enum class WifiAuthTypeBelowTiramisu(private val stringRep: String) : AuthenticationMode { + OPEN("Open"), + WEP("WEP"), + WPA_PSK("WPA-PSK"), + WPA2_PSK("WPA2-PSK"), + WPA_EAP("WPA-EAP"), + WPA_WPA2_PSK("WPA/WPA2-PSK"), + WPA2_EAP("WPA2-EAP"), + WPA3_PSK("WPA3-PSK"); + + companion object { + + /** + * @return The security of a given [ScanResult]. + */ + fun getSecurityTypes(scanResult: ScanResult): WifiAuthTypeBelowTiramisu { + val cap = scanResult.capabilities + val securityModes = entries.map { it.stringRep } + for (i in securityModes.indices.reversed()) { + if (cap.contains(securityModes[i])) { + WifiAuthTypeBelowTiramisu.entries.find { it.stringRep == securityModes[i] } + ?.let { + return it + } + } + } + return OPEN + } + + } +} + +/** + * Enum class that represents the authentication mode of a wifi network. + * + * Note: This is for Build version sdk Tiramisu and above. + */ +@Parcelize +enum class WifiAuthTypeTiramisuOrAbove(val id: Int) : AuthenticationMode { + UNKNOWN(-1), + OPEN(0), + WEP(1), // Shared + WPA_PSK(2), + WPA2_EAP(3), + WPA3_PSK(4), // WPA3-Personal (SAE) password) + EAP_WPA3_ENTERPRISE_192_BIT(5), + OWE(6), // Opportunistic Wireless Encryption + WAPI_PSK(7), // WAPI pre-shared key (PSK) + WAPI_CERT(8), // WAPI certificate to be specified. + EAP_WPA3_ENTERPRISE(9), + OSEN(10), // Hotspot 2. + PASSPOINT_R1_R2(11), + PASSPOINT_R3(12), + DPP(13); // Security type for Easy Connect (DPP) network + + companion object { + + /** + * Returns the authentication for provided security type. + */ + fun getMatchedAuthMode(securityTypes: IntArray): List { + val matchedType = mutableListOf() + securityTypes.forEach { type -> + val authMode = WifiAuthTypeTiramisuOrAbove.entries.find { it.id == type } + if (authMode != null) { + matchedType.add(authMode) + } + } + return matchedType.toList() + } + } +} diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt index 0ba5e4d4..46f99bdc 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt @@ -14,6 +14,6 @@ import kotlinx.parcelize.Parcelize data class WifiData( val ssid: String, val password: String, - val authType: String = "", + val authType: AuthenticationMode, val encryptionMode: String = "", ) : Parcelable \ No newline at end of file diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt index a7d18753..ffd51fac 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt @@ -61,12 +61,4 @@ internal object WifiHandoverDataType { */ const val MAC_ADDRESS_FIELD_ID: Short = 0x1020 const val MAX_MAC_ADDRESS_SIZE_BYTES = 6 - - // Constants for the authentication types. - const val WPA_PSK = "WPA-Personal" - const val WPA_EAP = "WPA-Enterprise" - const val WPA2_PSK = "WPA2-Personal" - const val WPA2_ENTERPRISE = "WPA2-Enterprise" - const val WPA_WPA2_PSK = "WPA/WPA2-Personal" - const val WEP = "Shared" } From bab7a8d5922f269d99ccb5e5a1769f6b99b5780e Mon Sep 17 00:00:00 2001 From: hiar Date: Wed, 5 Jun 2024 16:33:16 +0200 Subject: [PATCH 57/78] Changed to default dropdown --- .../feature/nfc/uicomponent/DropDownView.kt | 155 ++++++------------ 1 file changed, 48 insertions(+), 107 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt index 18a60ece..d774d4bf 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt @@ -1,46 +1,29 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Error +import androidx.compose.foundation.layout.size +import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MenuDefaults import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import no.nordicsemi.android.common.theme.NordicTheme /** @@ -50,8 +33,6 @@ import no.nordicsemi.android.common.theme.NordicTheme * @param label The label to be shown in the dropdown. * @param placeholder The placeholder to be shown in the dropdown. * @param defaultSelectedItem The default selected item in the dropdown. - * @param isError The flag to show error state. - * @param errorMessage The error message to be shown. * @param onItemSelected The callback to be called when an item is selected. */ @Composable @@ -60,115 +41,77 @@ internal inline fun DropdownView( label: String, placeholder: String, defaultSelectedItem: T? = null, - isError: Boolean = false, - errorMessage: String = "", crossinline onItemSelected: (T) -> Unit, ) { - DropDownMenu( + NfcDropdownMenu( items = items, label = label, + defaultSelectedItem = defaultSelectedItem, placeholder = placeholder, - itemSelected = defaultSelectedItem, - isError = isError, - errorMessage = errorMessage, - onItemSelected = { onItemSelected(it) }, + onItemSelected = { onItemSelected(it) } ) } -// Inspired from https://proandroiddev.com/improving-the-compose-dropdownmenu-88469b1ef34 @OptIn(ExperimentalMaterial3Api::class) @Composable -private inline fun DropDownMenu( +private fun NfcDropdownMenu( items: List, label: String, + defaultSelectedItem: T? = null, placeholder: String, - itemSelected: T? = null, - isError: Boolean = false, - errorMessage: String = "", - crossinline onItemSelected: (T) -> Unit, + onItemSelected: (T) -> Unit ) { - var expanded by remember { mutableStateOf(false) } - var selectedText by rememberSaveable { mutableStateOf(itemSelected) } - var selectedIndex by rememberSaveable { mutableIntStateOf(items.indexOfFirst { it == selectedText }) } + var expanded by rememberSaveable { mutableStateOf(false) } + var selectedText by rememberSaveable { mutableStateOf(defaultSelectedItem) } - Box(modifier = Modifier.height(IntrinsicSize.Min)) { - OutlinedTextField( - value = selectedText?.toString() ?: placeholder, - onValueChange = { }, - readOnly = true, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - modifier = Modifier.fillMaxWidth(), - placeholder = { Text(text = placeholder) }, - label = { Text(text = label) }, - isError = isError, - supportingText = { - if (isError) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Icon( - imageVector = Icons.Default.Error, - contentDescription = null, - tint = MaterialTheme.colorScheme.error, - ) - Text( - text = errorMessage, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - ) - } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Box { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded } - }, - ) - // Transparent clickable surface on top of OutlinedTextField - Surface( - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.shapes.extraSmall) - .clickable { expanded = true }, - color = Color.Transparent, - ) { } - } - - AnimatedVisibility(visible = expanded) { - Dialog(onDismissRequest = { expanded = false }) { - Surface(shape = RoundedCornerShape(12.dp)) { - val listState = rememberLazyListState() - if (selectedIndex > -1) { - LaunchedEffect("ScrollToSelected") { - listState.scrollToItem(index = selectedIndex) - } - } - - LazyColumn(modifier = Modifier.fillMaxWidth(), state = listState) { - itemsIndexed(items) { index, item -> - val selectedItem = index == selectedIndex - val (backgroundColor, itemColor) = if (selectedItem) { - MaterialTheme.colorScheme.primary to MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.surface to MaterialTheme.colorScheme.onSurface - } + ) { + OutlinedTextField( + value = selectedText?.toString() ?: placeholder, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier + .menuAnchor() + .fillMaxWidth(), + placeholder = { + Text(text = placeholder) + }, + label = { Text(text = label) }, + ) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier + .exposedDropdownSize() + .background(MaterialTheme.colorScheme.surface) + ) { + items.forEach { item -> DropdownMenuItem( - colors = MenuDefaults.itemColors(itemColor), - modifier = Modifier.background(backgroundColor), - text = { Text(item.toString()) }, + text = { + Text(item.toString()) + }, onClick = { selectedText = item - selectedIndex = index expanded = false onItemSelected(item) } ) - - if (index < items.lastIndex) { - HorizontalDivider() - } } } } } } + Spacer(modifier = Modifier.size(8.dp)) } @Preview @@ -179,9 +122,7 @@ private fun NfcDropDownViewPreview() { items = listOf("English", "Spanish", "French"), label = "Language", defaultSelectedItem = "English", - isError = true, placeholder = "Select language", - errorMessage = "Error message" ) {} } } \ No newline at end of file From fd37f952aa5859b145181be896329f0754848717 Mon Sep 17 00:00:00 2001 From: hiar Date: Wed, 5 Jun 2024 16:37:59 +0200 Subject: [PATCH 58/78] Removed !! operator from nfcAdapter --- .../android/wifi/provisioner/feature/nfc/di/NfcAdapter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/di/NfcAdapter.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/di/NfcAdapter.kt index efc3a369..0b290d26 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/di/NfcAdapter.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/di/NfcAdapter.kt @@ -16,7 +16,7 @@ object NfcAdapterModule { @Provides @Singleton - fun provideNfcAdapter(@ApplicationContext context: Context): NfcAdapter? { + fun provideNfcAdapter(@ApplicationContext context: Context): NfcAdapter { return NfcAdapter.getDefaultAdapter(context) } @@ -24,6 +24,6 @@ object NfcAdapterModule { @Singleton fun provideNfcManagerForWifi(@ApplicationContext context: Context) = NfcManagerForWifi( - nfcAdapter = provideNfcAdapter(context)!! + nfcAdapter = provideNfcAdapter(context) ) } \ No newline at end of file From 2ed36a4c278ef34e600832ab291e31bfaddbe48c Mon Sep 17 00:00:00 2001 From: hiar Date: Wed, 5 Jun 2024 16:40:57 +0200 Subject: [PATCH 59/78] Change string --- feature/nfc/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index 4f772c39..99cff078 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -51,7 +51,7 @@ Discover tag Tap an NFC tag Tag written successfully - It might take a few seconds to connect to the wifi network + The device may take a few seconds to connect to Wi-Fi. Discovering tag… Failed to write to the NFC tag From 2303e069e7649a3a24fa91706f3c8db39c61f21d Mon Sep 17 00:00:00 2001 From: hiar Date: Wed, 5 Jun 2024 16:47:39 +0200 Subject: [PATCH 60/78] Show lock icon for protected networks only --- .../provisioner/feature/nfc/view/WifiScannerView.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index 90736216..2ccec375 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -26,7 +26,6 @@ import androidx.compose.material.icons.outlined.ExpandMore import androidx.compose.material.icons.outlined.Group import androidx.compose.material.icons.outlined.GroupRemove import androidx.compose.material.icons.outlined.Lock -import androidx.compose.material.icons.outlined.Wifi import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider @@ -283,10 +282,13 @@ private fun NetworkItem( ) } Spacer(modifier = Modifier.weight(1f)) - Icon( - imageVector = if (isProtected) Icons.Outlined.Lock else Icons.Outlined.Wifi, - contentDescription = null, - ) + if (isProtected) { + // Show the lock icon for protected networks. + Icon( + imageVector = Icons.Outlined.Lock, + contentDescription = null, + ) + } } } From 36ad3e53a6e5564c64e6e481c32530092b83d7e3 Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 6 Jun 2024 12:03:11 +0200 Subject: [PATCH 61/78] Added macAddress --- .../nfc/uicomponent/AddWifiManuallyDialog.kt | 86 +++++++++++++++---- .../feature/nfc/uicomponent/PasswordDialog.kt | 1 + .../feature/nfc/uicomponent/TextInputField.kt | 61 +++++++++++++ .../feature/nfc/view/NfcPublishScreen.kt | 6 ++ .../feature/nfc/view/WifiScannerView.kt | 1 + .../nfc/viewmodel/WifiScannerViewModel.kt | 1 + feature/nfc/src/main/res/values/strings.xml | 3 + .../provisioner/nfc/NdefMessageBuilder.kt | 24 ++++-- .../wifi/provisioner/nfc/domain/WifiData.kt | 1 + .../nfc/domain/WifiHandoverDataType.kt | 2 +- 10 files changed, 161 insertions(+), 25 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt index 3e491985..44a7d7f8 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt @@ -15,10 +15,13 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -46,6 +49,8 @@ internal fun AddWifiManuallyDialog( var authMode by rememberSaveable { mutableStateOf("WPA2-Personal") } // default to WPA2-Personal. var encryptionMode by rememberSaveable { mutableStateOf(EncryptionMode.AES.toString()) } // default to AES. var isSsidEmpty by rememberSaveable { mutableStateOf(false) } + var macAddress by remember { mutableStateOf(TextFieldValue(text = "")) } + var isMacAddressError by rememberSaveable { mutableStateOf(false) } AlertDialog( onDismissRequest = { }, @@ -65,23 +70,6 @@ internal fun AddWifiManuallyDialog( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.verticalScroll(rememberScrollState()) ) { - val items = authListToDisplay() - // Show the authentication dropdown. - DropdownView( - items = items, - label = stringResource(id = R.string.authentication), - placeholder = stringResource(id = R.string.authentication_placeholder), - defaultSelectedItem = authMode - ) { authMode = it } - - // Show the encryption dropdown. - DropdownView( - items = EncryptionMode.entries.map { it.toString() }, - label = stringResource(id = R.string.encryption), - placeholder = stringResource(id = R.string.encryption_placeholder), - defaultSelectedItem = encryptionMode - ) { encryptionMode = it } - // Show the SSID field. TextInputField( input = ssid, @@ -94,6 +82,7 @@ internal fun AddWifiManuallyDialog( isSsidEmpty = ssid.isEmpty() } ) + // Show the password field only if the authentication mode is not open. if (authMode.lowercase() != "open") { // Show the password field. @@ -111,6 +100,40 @@ internal fun AddWifiManuallyDialog( // Clear the password if the authentication mode is open. password = "" } + + // Show the MAC address field. + TextInputField( + input = macAddress, + label = stringResource(id = R.string.mac_address_label), + placeholder = stringResource(id = R.string.mac_address_placeholder), + errorState = isMacAddressError && macAddress.text.isNotEmpty(), + errorMessage = stringResource(id = R.string.mac_address_error), + onUpdate = { + val value = addColonToMacAddress(it.text.uppercase()) + macAddress = TextFieldValue( + text = value, + selection = TextRange(value.length), + ) + isMacAddressError = !isValidMacAddress(value) + } + ) + + // Show the authentication dropdown. + DropdownView( + items = authListToDisplay(), + label = stringResource(id = R.string.authentication), + placeholder = stringResource(id = R.string.authentication_placeholder), + defaultSelectedItem = authMode + ) { authMode = it } + + // Show the encryption dropdown. + DropdownView( + items = EncryptionMode.entries.map { it.toString() }, + label = stringResource(id = R.string.encryption), + placeholder = stringResource(id = R.string.encryption_placeholder), + defaultSelectedItem = encryptionMode + ) { encryptionMode = it } + } }, dismissButton = { @@ -130,9 +153,13 @@ internal fun AddWifiManuallyDialog( authMode.lowercase() != "open" && password.trim() .isEmpty() -> isPasswordEmpty = true + macAddress.text.isNotEmpty() && !isValidMacAddress(macAddress.text) -> isMacAddressError = + true + else -> onConfirmClick( WifiData( ssid = ssid, + macAddress = macAddress.text, password = password, authType = authMode.toAuthenticationMode(), encryptionMode = encryptionMode, @@ -153,3 +180,28 @@ private fun OpenAddWifiManuallyDialogPreview() { onConfirmClick = {} ) } + +/** Adds colon to the MAC address. */ +private fun addColonToMacAddress(s: String, insertText: String = ":"): String { + val mac = s.replace(insertText, "") + val sb = StringBuilder() + for (i in mac.indices) { + sb.append(mac[i]) + if (i % 2 == 1 && i != mac.length - 1) { + sb.append(insertText) + } + } + return sb.toString() +} + +/** Checks if the MAC address is valid. */ +private fun isValidMacAddress(address: String): Boolean { + return try { + // Regex pattern to match a valid MAC address + val macAddressPattern = Regex("^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$") + return macAddressPattern.matches(address) + } catch (e: IllegalArgumentException) { + e.printStackTrace() + false + } +} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt index dd737933..5d0a9aae 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt @@ -102,6 +102,7 @@ internal fun PasswordDialog( onConfirmClick( WifiData( ssid = scanResult.SSID, + macAddress = scanResult.BSSID, password = password, authType = authMode.first(), encryptionMode = encryptionMode diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/TextInputField.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/TextInputField.kt index 6c8259c6..63a2aef0 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/TextInputField.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/TextInputField.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TransformedText import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp @@ -85,6 +86,66 @@ internal fun TextInputField( ) } +@Composable +fun TextInputField( + modifier: Modifier = Modifier, + input: TextFieldValue, + label: String, + hint: String = "", + placeholder: String = "", + errorMessage: String = "", + errorState: Boolean = false, + onUpdate: (TextFieldValue) -> Unit +) { + val textColor = MaterialTheme.colorScheme.onSurface.copy( + alpha = if (input.text.isEmpty()) 0.5f else LocalContentColor.current.alpha + ) + OutlinedTextField( + value = input, + onValueChange = { onUpdate(it) }, + visualTransformation = if (input.text.isEmpty()) + PlaceholderTransformation(placeholder) else VisualTransformation.None, + + modifier = modifier + .fillMaxWidth(), + label = { Text(text = label) }, + placeholder = { + Text( + text = placeholder, + ) + }, + supportingText = { + Column { + if (errorState) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.alpha(1f) + ) + } + } + if (hint.isNotEmpty() && !errorState) { + Text( + text = hint, + modifier = Modifier.alpha(0.38f) + ) + } + } + }, + colors = OutlinedTextFieldDefaults.colors(textColor), + isError = errorState, + ) +} + class PlaceholderTransformation(private val placeholder: String) : VisualTransformation { override fun filter(text: AnnotatedString): TransformedText { return placeholderFilter(placeholder) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt index 0e73147c..9951dad2 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt @@ -103,6 +103,12 @@ internal fun NfcPublishScreen() { if (wifiData.password.isNotEmpty()) { NfcPasswordRow(title = stringResource(id = R.string.password_title)) } + if (wifiData.macAddress.isNotEmpty()) { + NfcTextRow( + title = stringResource(id = R.string.mac_address_label), + text = wifiData.macAddress.uppercase() + ) + } NfcTextRow( title = stringResource(id = R.string.authentication_title), text = wifiData.authType.toDisplayString() diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index 2ccec375..9f3d9080 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -245,6 +245,7 @@ private fun NetworkItem( // Password dialog is not required for open networks. val wifiData = WifiData( ssid = network.SSID, + macAddress = network.BSSID, password = "", // Empty password for open networks authType = WifiAuthTypeBelowTiramisu.OPEN, ) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index f5bed605..e4a66f9e 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -118,6 +118,7 @@ internal class WifiScannerViewModel @Inject constructor( navigateToNfcScan( WifiData( ssid = event.network.SSID, + macAddress = event.network.BSSID, password = "", // Empty password for open network. authType = WifiAuthTypeBelowTiramisu.OPEN, encryptionMode = EncryptionMode.NONE.toString() // No encryption for open network. diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index 99cff078..ef5fe028 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -34,6 +34,9 @@ SSID Enter SSID SSID is required + MAC address + Enter MAC address + The MAC address consists of hexadecimal digits in the format XX:XX:XX:XX:XX:XX. Encryption Select encryption Password diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt index 7dea3511..1d62e969 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt @@ -22,6 +22,7 @@ import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.EN import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.ENC_TYPE_NONE import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.ENC_TYPE_TKIP import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.ENC_TYPE_WEP +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.MAC_ADDRESS_FIELD_ID import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.MAX_MAC_ADDRESS_SIZE_BYTES import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.NETWORK_KEY_FIELD_ID import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.NFC_TOKEN_MIME_TYPE @@ -74,16 +75,12 @@ class NdefMessageBuilder { val authType: Short = getAuthBytes(wifiNetwork.authType) val networkKey: String = wifiNetwork.password val networkKeySize = networkKey.toByteArray().size.toShort() - val encType = getEncByte(wifiNetwork.encryptionMode) - val macAddress = ByteArray(MAX_MAC_ADDRESS_SIZE_BYTES) - for (i in 0 until MAX_MAC_ADDRESS_SIZE_BYTES) { - macAddress[i] = 0xff.toByte() - } - + val macAddressBufferSize = if (wifiNetwork.macAddress.isNotEmpty()) 10 else 0 /* Fill buffer */ - val bufferSize = 24 + ssidSize + networkKeySize // size of required credential attributes + // size of required credential attributes + val bufferSize = 24 + ssidSize + networkKeySize + macAddressBufferSize // Create a buffer with the required size val buffer = ByteBuffer.allocate(bufferSize) @@ -110,6 +107,19 @@ class NdefMessageBuilder { buffer.putShort(networkKeySize) buffer.put(networkKey.toByteArray()) + // Add MAC address if available + if (wifiNetwork.macAddress.isNotEmpty()) { + // Convert the MAC address string to a ByteArray + val macAddress = wifiNetwork.macAddress.split(":") + .map { it.toInt(16).toByte() } + .toByteArray() + + // Add the MAC address + buffer.putShort(MAC_ADDRESS_FIELD_ID) + buffer.putShort(MAX_MAC_ADDRESS_SIZE_BYTES) + buffer.put(macAddress) + } + return buffer.array() } diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt index 46f99bdc..2d73afd4 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt @@ -14,6 +14,7 @@ import kotlinx.parcelize.Parcelize data class WifiData( val ssid: String, val password: String, + val macAddress: String, val authType: AuthenticationMode, val encryptionMode: String = "", ) : Parcelable \ No newline at end of file diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt index ffd51fac..1147e4fa 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiHandoverDataType.kt @@ -60,5 +60,5 @@ internal object WifiHandoverDataType { * The MAC Address field ID. */ const val MAC_ADDRESS_FIELD_ID: Short = 0x1020 - const val MAX_MAC_ADDRESS_SIZE_BYTES = 6 + const val MAX_MAC_ADDRESS_SIZE_BYTES: Short = 6 } From 190a5f8808a003e2bce0c4506c3ccb5d92f90979 Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 6 Jun 2024 12:29:56 +0200 Subject: [PATCH 62/78] Fixed icon positioning --- .../provisioner/feature/nfc/uicomponent/OutlinedCardItem.kt | 2 +- .../android/wifi/provisioner/feature/nfc/view/NfcScreen.kt | 3 --- feature/nfc/src/main/res/values/strings.xml | 4 ++-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/OutlinedCardItem.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/OutlinedCardItem.kt index 55c67eea..bca6f294 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/OutlinedCardItem.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/OutlinedCardItem.kt @@ -61,7 +61,7 @@ fun OutlinedCardItem( modifier = Modifier.size(28.dp) ) Column( - modifier = Modifier.padding(8.dp) + modifier = Modifier.padding(8.dp).weight(1f) ) { Text( text = headline, diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt index 81fd18a6..cb714579 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt @@ -5,7 +5,6 @@ import androidx.annotation.RequiresApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -86,7 +85,6 @@ internal fun NfcScreen() { icon = Icons.Default.Wifi, onCardClick = { isDialogOpen = true } ) { - Spacer(Modifier.weight(1f)) Icon( imageVector = Icons.Default.Add, contentDescription = null, @@ -111,7 +109,6 @@ internal fun NfcScreen() { icon = Icons.Default.WifiFind, onCardClick = { onEvent(OnScanClickEvent) } ) { - Spacer(Modifier.weight(1f)) Icon( imageVector = Icons.Default.Search, contentDescription = null, diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index ef5fe028..d7c474ad 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -6,8 +6,8 @@ Enter Wi-Fi credentials Enter the Wi-Fi credentials manually. - Search for Wi-Fi networks - Search for Wi-Fi networks. + Search Wi-Fi networks + Scans for nearby Wi-Fi networks. LOCATION PERMISSION REQUIRED From d169dbacbe2cbc33672fbb50bc36b76e0ea5e19c Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 6 Jun 2024 15:17:01 +0200 Subject: [PATCH 63/78] Show channel number and frequency --- .../feature/nfc/mapping/Frequency.kt | 171 ++++++++++++++++++ .../feature/nfc/view/WifiScannerView.kt | 21 ++- feature/nfc/src/main/res/values/strings.xml | 3 +- 3 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/mapping/Frequency.kt diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/mapping/Frequency.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/mapping/Frequency.kt new file mode 100644 index 00000000..0aded127 --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/mapping/Frequency.kt @@ -0,0 +1,171 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.mapping + +/** + * Frequency utility class to provide frequency band information. + * Inspired from [here](https://cs.android.com/android/platform/superproject/+/master:packages/modules/Wifi/framework/java/android/net/wifi/ScanResult.java) + */ +object Frequency { + /** The unspecified value. */ + private const val UNSPECIFIED: Int = -1 + + /** 2.4 GHz band first channel number. */ + private const val BAND_24_GHZ_FIRST_CH_NUM: Int = 1 + + /** 2.4 GHz band frequency of first channel in MHz. */ + private const val BAND_24_GHZ_START_FREQ_MHZ: Int = 2412 + + /** 2.4 GHz band frequency of last channel in MHz. */ + private const val BAND_24_GHZ_END_FREQ_MHZ: Int = 2484 + + /** 5 GHz band first channel number. */ + private const val BAND_5_GHZ_FIRST_CH_NUM: Int = 32 + + /** 5 GHz band frequency of first channel in MHz. */ + private const val BAND_5_GHZ_START_FREQ_MHZ: Int = 5160 + + /** 5 GHz band frequency of last channel in MHz. */ + private const val BAND_5_GHZ_END_FREQ_MHZ: Int = 5885 + + /** 6 GHz band first channel number. */ + private const val BAND_6_GHZ_FIRST_CH_NUM: Int = 1 + + /** 6 GHz band frequency of first channel in MHz. */ + private const val BAND_6_GHZ_START_FREQ_MHZ: Int = 5955 + + /** 6 GHz band frequency of last channel in MHz. */ + private const val BAND_6_GHZ_END_FREQ_MHZ: Int = 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. + */ + private const val BAND_6_GHZ_PSC_START_MHZ: Int = 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. + */ + private const val BAND_6_GHZ_PSC_STEP_SIZE_MHZ: Int = 80 + + /** 6 GHz band operating class 136 channel 2 center frequency in MHz. */ + private const val BAND_6_GHZ_OP_CLASS_136_CH_2_FREQ_MHZ: Int = 5935 + + /** 60 GHz band first channel number. */ + private const val BAND_60_GHZ_FIRST_CH_NUM: Int = 1 + + /** 60 GHz band frequency of first channel in MHz. */ + private const val BAND_60_GHZ_START_FREQ_MHZ: Int = 58320 + + /** 60 GHz band frequency of last channel in MHz. */ + private const val BAND_60_GHZ_END_FREQ_MHZ: Int = 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 + */ + private fun is24GHz(freqMhz: Int): Boolean { + return freqMhz in BAND_24_GHZ_START_FREQ_MHZ..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 + */ + private fun is5GHz(freqMhz: Int): Boolean { + return freqMhz in BAND_5_GHZ_START_FREQ_MHZ..BAND_5_GHZ_END_FREQ_MHZ + } + + /** + * Utility function to check if a frequency within 6 GHz band. + * + * @param freqMhz + * @return true if within 6GHz, false otherwise + */ + private fun is6GHz(freqMhz: Int): Boolean { + if (freqMhz == BAND_6_GHZ_OP_CLASS_136_CH_2_FREQ_MHZ) { + return true + } + return (freqMhz in BAND_6_GHZ_START_FREQ_MHZ..BAND_6_GHZ_END_FREQ_MHZ) + } + + /** + * Utility function to check if a frequency is 6Ghz PSC channel. + * + * @param freqMhz + * @return true if the frequency is 6GHz PSC, false otherwise + */ + private fun is6GHzPsc(freqMhz: Int): Boolean { + 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 + * @param freqMhz + * @return true if within 60GHz, false otherwise + * + * @hide + */ + private fun is60GHz(freqMhz: Int): Boolean { + return freqMhz in BAND_60_GHZ_START_FREQ_MHZ..BAND_60_GHZ_END_FREQ_MHZ + } + + /** + * Utility function to get the frequency band of a given frequency. + * + * @param frequency frequency in MHz + * @return frequency band + */ + fun get(frequency: Int): String { + return when { + is24GHz(frequency) -> "2.4 GHz" + is5GHz(frequency) -> "5 GHz" + is6GHz(frequency) -> "6 GHz" + is6GHzPsc(frequency) -> "6 GHz PSC" + is60GHz(frequency) -> "60 GHz" + else -> "Unknown" + } + } + + /** + * Utility function to convert frequency in MHz to channel number. + * + * @param freqMhz frequency in MHz + * @return channel number associated with given frequency, [.UNSPECIFIED] if no match + */ + fun toChannelNumber(freqMhz: Int): Int { + when { + freqMhz == 2484 -> { + return 14 + } + + is24GHz(freqMhz) -> { + return (freqMhz - BAND_24_GHZ_START_FREQ_MHZ) / 5 + BAND_24_GHZ_FIRST_CH_NUM + } + + is5GHz(freqMhz) -> { + return ((freqMhz - BAND_5_GHZ_START_FREQ_MHZ) / 5) + BAND_5_GHZ_FIRST_CH_NUM + } + + 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 + } + + is60GHz(freqMhz) -> { + return ((freqMhz - BAND_60_GHZ_START_FREQ_MHZ) / 2160) + BAND_60_GHZ_FIRST_CH_NUM + } + + else -> return UNSPECIFIED + } + } + +} diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index 9f3d9080..8a672f0a 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -51,6 +51,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import no.nordicsemi.android.common.theme.view.NordicAppBar import no.nordicsemi.android.wifi.provisioner.feature.nfc.R +import no.nordicsemi.android.wifi.provisioner.feature.nfc.mapping.Frequency import no.nordicsemi.android.wifi.provisioner.feature.nfc.mapping.toDisplayString import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireLocationForWifi import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireWifi @@ -164,7 +165,8 @@ internal fun WifiScannerScreen() { } Column( modifier = Modifier - .verticalScroll(rememberScrollState()), + .verticalScroll(rememberScrollState()) + .padding(bottom = 32.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { if (isGroupedBySsid) { @@ -255,7 +257,9 @@ private fun NetworkItem( ) { RssiIconView(network.level) Column( - modifier = Modifier.padding(8.dp), + modifier = Modifier + .padding(8.dp) + .weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp) ) { // Display the SSID of the access point @@ -273,16 +277,25 @@ private fun NetworkItem( // Display the address of the access point. Text( text = network.BSSID.uppercase(), - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodyMedium, modifier = Modifier.alpha(0.7f) ) // Display the security type of the access point. Text( text = securityType.joinToString(", ") { it.toDisplayString() }, + style = MaterialTheme.typography.bodyMedium, modifier = Modifier.alpha(0.7f) ) + Text( + text = stringResource( + id = R.string.channel_number, + Frequency.toChannelNumber(network.frequency), + Frequency.get(network.frequency) + ), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.alpha(0.7f), + ) } - Spacer(modifier = Modifier.weight(1f)) if (isProtected) { // Show the lock icon for protected networks. Icon( diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index d7c474ad..5566990e 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -19,7 +19,7 @@ Wi-Fi is disabled. Please enable Wi-Fi to scan for Wi-Fi networks. Enable Wi-Fi WI-FI NOT AVAILABLE - Wi-Fi is not available on this device.We won\'t be able to provision the device. + Wi-Fi is not available on this device. We won\'t be able to provision the device. Grant Permission Settings @@ -42,6 +42,7 @@ Password Enter password Password is required + Channel number: %d - %s Wi-Fi record From 67740f13074bd2660f52c3e59a22c41684960a7b Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 6 Jun 2024 15:46:13 +0200 Subject: [PATCH 64/78] Added encryption as WifiData type --- .../feature/nfc/mapping/UiMapper.kt | 27 +++++++++ .../nfc/uicomponent/AddWifiManuallyDialog.kt | 8 ++- .../feature/nfc/uicomponent/PasswordDialog.kt | 2 +- .../feature/nfc/view/NfcPublishScreen.kt | 10 ++-- .../feature/nfc/view/WifiScannerView.kt | 2 + .../nfc/viewmodel/WifiScannerViewModel.kt | 2 +- .../provisioner/nfc/NdefMessageBuilder.kt | 20 +++---- .../provisioner/nfc/domain/EncryptionMode.kt | 59 +++++++++++-------- .../wifi/provisioner/nfc/domain/WifiData.kt | 2 +- 9 files changed, 82 insertions(+), 50 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/mapping/UiMapper.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/mapping/UiMapper.kt index feea296c..2ef36024 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/mapping/UiMapper.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/mapping/UiMapper.kt @@ -1,6 +1,7 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.mapping import no.nordicsemi.android.wifi.provisioner.nfc.domain.AuthenticationMode +import no.nordicsemi.android.wifi.provisioner.nfc.domain.EncryptionMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeBelowTiramisu import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeTiramisuOrAbove @@ -51,3 +52,29 @@ fun String.toAuthenticationMode(): AuthenticationMode = when (this) { "WPA3-Personal" -> WifiAuthTypeBelowTiramisu.WPA3_PSK else -> WifiAuthTypeBelowTiramisu.OPEN } + +/** + * Converts the [EncryptionMode] to a display string. + * + * @return The display string. + */ +fun EncryptionMode.toDisplayString(): String = when (this) { + EncryptionMode.NONE -> "None" + EncryptionMode.WEP -> "WEP" + EncryptionMode.TKIP -> "TKIP" + EncryptionMode.AES -> "AES" + EncryptionMode.AES_TKIP -> "AES/TKIP" +} + +/** + * Converts the display string to [EncryptionMode]. + * + * @return The [EncryptionMode]. + */ +fun String.toEncryptionMode(): EncryptionMode = when (this) { + "WEP" -> EncryptionMode.WEP + "TKIP" -> EncryptionMode.TKIP + "AES" -> EncryptionMode.AES + "AES/TKIP" -> EncryptionMode.AES_TKIP + else -> EncryptionMode.NONE +} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt index 44a7d7f8..a1bba84a 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt @@ -28,6 +28,8 @@ import androidx.compose.ui.unit.dp import no.nordicsemi.android.wifi.provisioner.feature.nfc.R import no.nordicsemi.android.wifi.provisioner.feature.nfc.mapping.authListToDisplay import no.nordicsemi.android.wifi.provisioner.feature.nfc.mapping.toAuthenticationMode +import no.nordicsemi.android.wifi.provisioner.feature.nfc.mapping.toDisplayString +import no.nordicsemi.android.wifi.provisioner.feature.nfc.mapping.toEncryptionMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.EncryptionMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData @@ -47,7 +49,7 @@ internal fun AddWifiManuallyDialog( var showPassword by rememberSaveable { mutableStateOf(false) } var isPasswordEmpty by rememberSaveable { mutableStateOf(false) } var authMode by rememberSaveable { mutableStateOf("WPA2-Personal") } // default to WPA2-Personal. - var encryptionMode by rememberSaveable { mutableStateOf(EncryptionMode.AES.toString()) } // default to AES. + var encryptionMode by rememberSaveable { mutableStateOf(EncryptionMode.AES.toDisplayString()) } // default to AES. var isSsidEmpty by rememberSaveable { mutableStateOf(false) } var macAddress by remember { mutableStateOf(TextFieldValue(text = "")) } var isMacAddressError by rememberSaveable { mutableStateOf(false) } @@ -128,7 +130,7 @@ internal fun AddWifiManuallyDialog( // Show the encryption dropdown. DropdownView( - items = EncryptionMode.entries.map { it.toString() }, + items = EncryptionMode.entries.map { it.toDisplayString() }, label = stringResource(id = R.string.encryption), placeholder = stringResource(id = R.string.encryption_placeholder), defaultSelectedItem = encryptionMode @@ -162,7 +164,7 @@ internal fun AddWifiManuallyDialog( macAddress = macAddress.text, password = password, authType = authMode.toAuthenticationMode(), - encryptionMode = encryptionMode, + encryptionMode = encryptionMode.toEncryptionMode(), ) ) } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt index 5d0a9aae..d7431cd3 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/PasswordDialog.kt @@ -42,7 +42,7 @@ internal fun PasswordDialog( var password by rememberSaveable { mutableStateOf("") } var isPasswordEmpty by rememberSaveable { mutableStateOf(false) } val authMode = AuthenticationMode.get(scanResult) - val encryptionMode = EncryptionMode.getEncryption(scanResult) + val encryptionMode = EncryptionMode.get(scanResult) AlertDialog( onDismissRequest = { }, diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt index 9951dad2..94f4433c 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt @@ -113,12 +113,10 @@ internal fun NfcPublishScreen() { title = stringResource(id = R.string.authentication_title), text = wifiData.authType.toDisplayString() ) - if (wifiData.encryptionMode.isNotEmpty()) { - NfcTextRow( - title = stringResource(id = R.string.encryption_title), - text = wifiData.encryptionMode - ) - } + NfcTextRow( + title = stringResource(id = R.string.encryption_title), + text = wifiData.encryptionMode.toDisplayString() + ) NfcTextRow( title = stringResource(id = R.string.message_size), text = stringResource( diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index 8a672f0a..3dbbfbe7 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -65,6 +65,7 @@ import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnSortOption import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.WifiScannerViewModel import no.nordicsemi.android.wifi.provisioner.nfc.domain.AuthenticationMode +import no.nordicsemi.android.wifi.provisioner.nfc.domain.EncryptionMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success @@ -250,6 +251,7 @@ private fun NetworkItem( macAddress = network.BSSID, password = "", // Empty password for open networks authType = WifiAuthTypeBelowTiramisu.OPEN, + encryptionMode = EncryptionMode.NONE ) onEvent(OnPasswordSetEvent(wifiData)) } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index e4a66f9e..5bf90ee8 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -121,7 +121,7 @@ internal class WifiScannerViewModel @Inject constructor( macAddress = event.network.BSSID, password = "", // Empty password for open network. authType = WifiAuthTypeBelowTiramisu.OPEN, - encryptionMode = EncryptionMode.NONE.toString() // No encryption for open network. + encryptionMode = EncryptionMode.NONE // No encryption for open network. ) ) } else { diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt index 1d62e969..bd85e2cb 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/NdefMessageBuilder.kt @@ -7,6 +7,7 @@ import no.nordicsemi.android.wifi.provisioner.nfc.domain.EncryptionMode import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeBelowTiramisu import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeTiramisuOrAbove import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_EXPECTED_SIZE import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_FIELD_ID import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_OPEN import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiHandoverDataType.AUTH_TYPE_SHARED @@ -94,7 +95,7 @@ class NdefMessageBuilder { // Add authentication type buffer.putShort(AUTH_TYPE_FIELD_ID) - buffer.putShort(2.toShort()) + buffer.putShort(AUTH_TYPE_EXPECTED_SIZE) buffer.putShort(authType) // Add encryption type @@ -128,18 +129,13 @@ class NdefMessageBuilder { * * @param enc the encryption type. */ - private fun getEncByte(enc: String): Short { - - // If Empty, default to AES - if (enc.isEmpty()) { - return ENC_TYPE_AES - } + private fun getEncByte(enc: EncryptionMode): Short { return when (enc) { - EncryptionMode.WEP.toString() -> ENC_TYPE_WEP - EncryptionMode.TKIP.toString() -> ENC_TYPE_TKIP - EncryptionMode.AES.toString() -> ENC_TYPE_AES - EncryptionMode.AES_TKIP.toString() -> ENC_TYPE_AES_TKIP - else -> ENC_TYPE_NONE + EncryptionMode.NONE -> ENC_TYPE_NONE + EncryptionMode.WEP -> ENC_TYPE_WEP + EncryptionMode.TKIP -> ENC_TYPE_TKIP + EncryptionMode.AES -> ENC_TYPE_AES + EncryptionMode.AES_TKIP -> ENC_TYPE_AES_TKIP } } diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt index 2ef2b66d..18486e5e 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/EncryptionMode.kt @@ -12,16 +12,15 @@ enum class EncryptionMode { AES, AES_TKIP; - override fun toString() = when (this) { - NONE -> "NONE" - WEP -> "WEP" - TKIP -> "TKIP" - AES -> "AES" - AES_TKIP -> "AES/TKIP" - - } - companion object { + // Constants for the encryption modes. + private const val WPA2_PSK = "WPA2-PSK" + private const val WPA3_PSK = "WPA3-PSK" + private const val WPA2_EAP = "WPA2-EAP" + private const val WPA_EAP = "WPA2-EAP" + private const val WPA_PSK = "WPA-PSK" + private const val WPA_WPA2_PSK = "WPA/WPA2-PSK" + private const val WEP_S = "WEP" /** * Returns the encryption mode of the given scan result. @@ -35,26 +34,34 @@ enum class EncryptionMode { * @param result the scan result. * @return the encryption mode. */ - fun getEncryption(result: ScanResult): String { - val firstCapabilities = - result.capabilities.substring(1, result.capabilities.indexOf("]")) - val capabilities = firstCapabilities.split("-".toRegex()).dropLastWhile { it.isEmpty() } - .toTypedArray() - val auth = capabilities[0] + "-" + capabilities[1] - val encryptionMode = when { - auth.contains("WPA2-PSK") || - auth.contains("WPA3-PSK") || - auth.contains("WPA2-EAP") -> AES + fun get(result: ScanResult): EncryptionMode { + return try { + val firstCapabilities = + result.capabilities.substring(1, result.capabilities.indexOf("]")) + val capabilities = + firstCapabilities.split("-".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val auth = capabilities[0] + "-" + capabilities[1] + return when { + auth.contains(WPA2_PSK) || + auth.contains(WPA3_PSK) || + auth.contains(WPA2_EAP) -> AES - auth.contains("WPA-PSK") -> TKIP - auth.contains("WPA-EAP") || - auth.contains("WPA/WPA2-PSK") -> AES_TKIP + auth.contains(WPA_PSK) -> TKIP + auth.contains(WPA_EAP) || + auth.contains(WPA_WPA2_PSK) -> AES_TKIP - auth.contains("WEP") -> WEP - else -> NONE - } + auth.contains(WEP_S) -> WEP + else -> NONE + } - return encryptionMode.toString() + } catch (e: ArrayIndexOutOfBoundsException) { + // Handle the case where capabilities array doesn't have enough elements + NONE + } catch (e: StringIndexOutOfBoundsException) { + // Handle the case where substring or indexOf operations fail + NONE + } } } } diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt index 2d73afd4..27b7b0d0 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/domain/WifiData.kt @@ -16,5 +16,5 @@ data class WifiData( val password: String, val macAddress: String, val authType: AuthenticationMode, - val encryptionMode: String = "", + val encryptionMode: EncryptionMode, ) : Parcelable \ No newline at end of file From 407afdf1c3a665bc85425f1d255dba8bffcaa1a7 Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 6 Jun 2024 16:26:52 +0200 Subject: [PATCH 65/78] Default encryption to None when network is open --- .../nfc/uicomponent/AddWifiManuallyDialog.kt | 34 ++++++++++++++----- .../feature/nfc/uicomponent/DropDownView.kt | 5 ++- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt index a1bba84a..1c990297 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt @@ -126,16 +126,34 @@ internal fun AddWifiManuallyDialog( label = stringResource(id = R.string.authentication), placeholder = stringResource(id = R.string.authentication_placeholder), defaultSelectedItem = authMode - ) { authMode = it } + ) { + authMode = it + if (authMode.lowercase() == "open") { + // Clear the password if the authentication mode is open. + encryptionMode = EncryptionMode.NONE.toDisplayString() + } + } // Show the encryption dropdown. - DropdownView( - items = EncryptionMode.entries.map { it.toDisplayString() }, - label = stringResource(id = R.string.encryption), - placeholder = stringResource(id = R.string.encryption_placeholder), - defaultSelectedItem = encryptionMode - ) { encryptionMode = it } - + if (authMode.lowercase() == "open") { + // Disable the encryption dropdown if the authentication mode is open. + DropdownView( + items = EncryptionMode.entries.map { it.toDisplayString() }, + label = stringResource(id = R.string.encryption), + placeholder = stringResource(id = R.string.encryption_placeholder), + defaultSelectedItem = EncryptionMode.NONE.toDisplayString(), + isEnabled = false + ) { + } + } else { + // Show the encryption dropdown. + DropdownView( + items = EncryptionMode.entries.map { it.toDisplayString() }, + label = stringResource(id = R.string.encryption), + placeholder = stringResource(id = R.string.encryption_placeholder), + defaultSelectedItem = encryptionMode, + ) { encryptionMode = it } + } } }, dismissButton = { diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt index d774d4bf..89c2ca8e 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt @@ -41,6 +41,7 @@ internal inline fun DropdownView( label: String, placeholder: String, defaultSelectedItem: T? = null, + isEnabled: Boolean = true, crossinline onItemSelected: (T) -> Unit, ) { NfcDropdownMenu( @@ -48,6 +49,7 @@ internal inline fun DropdownView( label = label, defaultSelectedItem = defaultSelectedItem, placeholder = placeholder, + isEnabled = isEnabled, onItemSelected = { onItemSelected(it) } ) } @@ -59,6 +61,7 @@ private fun NfcDropdownMenu( label: String, defaultSelectedItem: T? = null, placeholder: String, + isEnabled: Boolean, onItemSelected: (T) -> Unit ) { var expanded by rememberSaveable { mutableStateOf(false) } @@ -72,7 +75,7 @@ private fun NfcDropdownMenu( ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { - expanded = !expanded + if (isEnabled) expanded = !expanded } ) { OutlinedTextField( From f3b97f2e4d8b3df761254bd2b25b5530aa75d003 Mon Sep 17 00:00:00 2001 From: hiar Date: Thu, 6 Jun 2024 16:57:33 +0200 Subject: [PATCH 66/78] Fixed permission state not updated immediately --- .../wifi/provisioner/feature/nfc/view/WifiScannerView.kt | 5 +++++ .../feature/nfc/viewmodel/WifiScannerViewModel.kt | 6 +----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index 3dbbfbe7..586e1019 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -36,6 +36,7 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -126,6 +127,10 @@ internal fun WifiScannerScreen() { ) { RequireWifi { RequireLocationForWifi { + LaunchedEffect(key1 = it) { + wifiScannerViewModel.scanAvailableWifiNetworks() + } + when (val scanningState = viewState.networks) { is Error -> { // Show the error message. diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index 5bf90ee8..fc99748a 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -85,14 +85,10 @@ internal class WifiScannerViewModel @Inject constructor( private val _viewState = MutableStateFlow(WifiScannerViewState()) val viewState = _viewState.asStateFlow() - init { - scanAvailableWifiNetworks() - } - /** * Scans for available Wi-Fi networks. */ - private fun scanAvailableWifiNetworks() { + fun scanAvailableWifiNetworks() { try { wifiManager.onScan() wifiManager.networkState.onEach { scanResults -> From 8895f7c30a47918889f6f7d66ed2ca7f7d434578 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 7 Jun 2024 10:17:28 +0200 Subject: [PATCH 67/78] Fixed continuous scanning --- .../wifi/provisioner/nfc/WifiManagerRepository.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt index 434ff05a..3ccede96 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt @@ -35,13 +35,8 @@ class WifiManagerRepository( @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) override fun onReceive(context: Context, intent: Intent) { try { - val success = - intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false) - if (!success) { - wifiManager.startScan() - } - val results = wifiManager.scanResults - _networkState.value = Success(results) + intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false) + _networkState.value = Success(wifiManager.scanResults) } catch (e: Exception) { e.printStackTrace() _networkState.value = Error(e) From 4a3cb172bf90ae3829d7212f581cdb20af4aecd8 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 7 Jun 2024 11:57:33 +0200 Subject: [PATCH 68/78] Added pull to refresh feature --- feature/nfc/build.gradle.kts | 2 +- .../feature/nfc/view/WifiScannerView.kt | 84 ++++++++++--------- .../nfc/viewmodel/WifiScannerViewModel.kt | 9 ++ feature/nfc/src/main/res/values/strings.xml | 2 +- 4 files changed, 55 insertions(+), 42 deletions(-) diff --git a/feature/nfc/build.gradle.kts b/feature/nfc/build.gradle.kts index a8a104b8..e988272a 100644 --- a/feature/nfc/build.gradle.kts +++ b/feature/nfc/build.gradle.kts @@ -18,6 +18,6 @@ dependencies { implementation(project(":feature:common")) implementation(project(":feature:ui")) api(project(":lib:nfc")) - api(project(":lib:domain")) // TODD: Remove this once feature:common is utilized in this module. implementation(libs.nordic.permissions.nfc) + implementation("androidx.compose.material3:material3:1.3.0-beta02") } \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index 586e1019..f31ddbc1 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -35,6 +35,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -120,10 +121,8 @@ internal fun WifiScannerScreen() { SnackbarHost(hostState = snackbarHostState) }, ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding), + Box( + modifier = Modifier.padding(innerPadding) ) { RequireWifi { RequireLocationForWifi { @@ -150,16 +149,14 @@ internal fun WifiScannerScreen() { is Loading -> { // Show the loading indicator. - Column { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - modifier = Modifier.padding(16.dp) - ) - } + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.padding(16.dp) + ) } } @@ -169,39 +166,46 @@ internal fun WifiScannerScreen() { WifiSortView(viewState.sortOption) { onEvent(OnSortOptionSelected(it)) } - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(bottom = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, + PullToRefreshBox( + isRefreshing = viewState.isRefreshing, + onRefresh = { // Refreshing + wifiScannerViewModel.scanAvailableWifiNetworks() + }, ) { - if (isGroupedBySsid) { - // Group by SSID - GroupBySsid(viewState.sortedItems, onEvent) - } else { - // Show the list of available networks grouped by SSIDs. - WifiList(viewState.sortedItems, onEvent) + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(bottom = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (isGroupedBySsid) { + // Group by SSID + GroupBySsid(viewState.sortedItems, onEvent) + } else { + // Show the list of available networks grouped by SSIDs. + WifiList(viewState.sortedItems, onEvent) + } } } } } } - } - when (val selectedNetwork = viewState.selectedNetwork) { - null -> { - // Do nothing - } + when (val selectedNetwork = viewState.selectedNetwork) { + null -> { + // Do nothing + } - else -> { - // Show the password dialog - PasswordDialog( - scanResult = selectedNetwork, - onCancelClick = { - // Dismiss the dialog - // Set the selected network to null - onEvent(OnPasswordCancelEvent) - }) { - onEvent(OnPasswordSetEvent(it)) + else -> { + // Show the password dialog + PasswordDialog( + scanResult = selectedNetwork, + onCancelClick = { + // Dismiss the dialog + // Set the selected network to null + onEvent(OnPasswordCancelEvent) + }) { + onEvent(OnPasswordSetEvent(it)) + } } } } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index fc99748a..5dc724a0 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -6,10 +6,12 @@ import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.wifi.provisioner.feature.nfc.NfcPublishDestination import no.nordicsemi.android.wifi.provisioner.feature.nfc.WifiScannerDestination @@ -24,6 +26,7 @@ import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeTiramisuOrA import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData import no.nordicsemi.kotlin.wifi.provisioner.feature.common.event.WifiSortOption import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds /** * A sealed class to represent the events that can be triggered from the UI. @@ -68,6 +71,7 @@ data class WifiScannerViewState( val networks: NetworkState> = Loading(), val selectedNetwork: ScanResult? = null, val sortOption: WifiSortOption = WifiSortOption.RSSI, + val isRefreshing: Boolean = false, ) { private val items = (networks as? Success)?.data ?: emptyList() val sortedItems: List = when (sortOption) { @@ -90,10 +94,15 @@ internal class WifiScannerViewModel @Inject constructor( */ fun scanAvailableWifiNetworks() { try { + _viewState.value = _viewState.value.copy(isRefreshing = true) wifiManager.onScan() wifiManager.networkState.onEach { scanResults -> _viewState.value = _viewState.value.copy(networks = scanResults) }.launchIn(viewModelScope) + viewModelScope.launch { + delay(2.seconds) + _viewState.value = _viewState.value.copy(isRefreshing = false) + } } catch (e: Exception) { e.printStackTrace() } diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index 5566990e..21b2e386 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -24,7 +24,7 @@ Grant Permission Settings - Error occurred while scanning for networks. + Error occurred while scanning Wi-Fi networks. Unknown error occurred. From 7f743055c601587c16621677857219933a2fa6df Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 7 Jun 2024 11:58:55 +0200 Subject: [PATCH 69/78] Added RequiresPermission --- .../android/wifi/provisioner/nfc/WifiManagerRepository.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt index 3ccede96..247cc16d 100644 --- a/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt +++ b/lib/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/nfc/WifiManagerRepository.kt @@ -1,5 +1,6 @@ package no.nordicsemi.android.wifi.provisioner.nfc +import android.Manifest import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -32,7 +33,12 @@ class WifiManagerRepository( */ private val wifiScanReceiver = object : BroadcastReceiver() { @RequiresApi(Build.VERSION_CODES.M) - @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) + @RequiresPermission( + allOf = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + ] + ) override fun onReceive(context: Context, intent: Intent) { try { intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false) From b71c543e3567c0c42c40e9c2b4cfa83e45f8f20f Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 7 Jun 2024 12:30:12 +0200 Subject: [PATCH 70/78] Code optimization --- .../nfc/uicomponent/AddWifiManuallyDialog.kt | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt index 1c990297..05e9b50f 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt @@ -126,27 +126,10 @@ internal fun AddWifiManuallyDialog( label = stringResource(id = R.string.authentication), placeholder = stringResource(id = R.string.authentication_placeholder), defaultSelectedItem = authMode - ) { - authMode = it - if (authMode.lowercase() == "open") { - // Clear the password if the authentication mode is open. - encryptionMode = EncryptionMode.NONE.toDisplayString() - } - } + ) { authMode = it } - // Show the encryption dropdown. - if (authMode.lowercase() == "open") { - // Disable the encryption dropdown if the authentication mode is open. - DropdownView( - items = EncryptionMode.entries.map { it.toDisplayString() }, - label = stringResource(id = R.string.encryption), - placeholder = stringResource(id = R.string.encryption_placeholder), - defaultSelectedItem = EncryptionMode.NONE.toDisplayString(), - isEnabled = false - ) { - } - } else { - // Show the encryption dropdown. + // Show the encryption dropdown, only for protected network. + if (authMode.lowercase() != "open") { DropdownView( items = EncryptionMode.entries.map { it.toDisplayString() }, label = stringResource(id = R.string.encryption), @@ -182,7 +165,8 @@ internal fun AddWifiManuallyDialog( macAddress = macAddress.text, password = password, authType = authMode.toAuthenticationMode(), - encryptionMode = encryptionMode.toEncryptionMode(), + encryptionMode = if (authMode.lowercase() == "open") + EncryptionMode.NONE else encryptionMode.toEncryptionMode(), ) ) } From 42f5e2195a507ba4eec47d4e05e517793935d303 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 7 Jun 2024 14:39:51 +0200 Subject: [PATCH 71/78] Removed unused parameter --- .../feature/nfc/uicomponent/DropDownView.kt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt index 89c2ca8e..805a2217 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/DropDownView.kt @@ -13,6 +13,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -41,7 +42,6 @@ internal inline fun DropdownView( label: String, placeholder: String, defaultSelectedItem: T? = null, - isEnabled: Boolean = true, crossinline onItemSelected: (T) -> Unit, ) { NfcDropdownMenu( @@ -49,7 +49,6 @@ internal inline fun DropdownView( label = label, defaultSelectedItem = defaultSelectedItem, placeholder = placeholder, - isEnabled = isEnabled, onItemSelected = { onItemSelected(it) } ) } @@ -61,7 +60,6 @@ private fun NfcDropdownMenu( label: String, defaultSelectedItem: T? = null, placeholder: String, - isEnabled: Boolean, onItemSelected: (T) -> Unit ) { var expanded by rememberSaveable { mutableStateOf(false) } @@ -74,9 +72,7 @@ private fun NfcDropdownMenu( Box { ExposedDropdownMenuBox( expanded = expanded, - onExpandedChange = { - if (isEnabled) expanded = !expanded - } + onExpandedChange = { expanded = it } ) { OutlinedTextField( value = selectedText?.toString() ?: placeholder, @@ -84,7 +80,7 @@ private fun NfcDropdownMenu( readOnly = true, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, modifier = Modifier - .menuAnchor() + .menuAnchor(MenuAnchorType.PrimaryNotEditable) .fillMaxWidth(), placeholder = { Text(text = placeholder) From a9bad37cb7ecff755c86ba1f4dfa71a26aff309a Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 7 Jun 2024 14:40:52 +0200 Subject: [PATCH 72/78] Fixed padding --- .../provisioner/feature/nfc/view/WifiScannerView.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index f31ddbc1..b37982ca 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -111,7 +110,7 @@ internal fun WifiScannerScreen() { .clickable { isGroupedBySsid = !isGroupedBySsid } - .padding(8.dp) + .padding(8.dp, 8.dp, 16.dp, 8.dp) ) } @@ -228,7 +227,7 @@ internal fun WifiList( networks.forEach { network -> NetworkItem( network = network, - modifier = Modifier.padding(8.dp), + modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp), onEvent = onEvent ) HorizontalDivider() @@ -345,17 +344,14 @@ private fun GroupBySsid( modifier = Modifier .fillMaxWidth() .clickable { isExpanded = !isExpanded } - .padding(8.dp) + .padding(start = 16.dp, 8.dp) ) { Text(text = ssid) Spacer(modifier = Modifier.weight(1f)) Icon( imageVector = expandIcon, contentDescription = null, - modifier = Modifier - .clip(CircleShape) - .clickable { isExpanded = !isExpanded } - .padding(8.dp) + modifier = Modifier.padding(8.dp, 8.dp, 16.dp, 8.dp) ) } // Show networks under the same SSID. From f17cff9e8f8304ae178bc298a2a1985c09315453 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 7 Jun 2024 14:41:37 +0200 Subject: [PATCH 73/78] Made lowercase ssid before sorting --- .../provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index 5dc724a0..41ce92e1 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -75,7 +75,7 @@ data class WifiScannerViewState( ) { private val items = (networks as? Success)?.data ?: emptyList() val sortedItems: List = when (sortOption) { - WifiSortOption.NAME -> items.sortedBy { it.SSID } + WifiSortOption.NAME -> items.sortedBy { it.SSID.lowercase() } WifiSortOption.RSSI -> items.sortedByDescending { it.level } } } From d60457f1fc7f7778ff7bd423d2758283b170f6e4 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 7 Jun 2024 14:46:01 +0200 Subject: [PATCH 74/78] Ui fixes --- .../nfc/uicomponent/AddWifiManuallyDialog.kt | 16 ++++++++-------- .../feature/nfc/view/NfcPublishScreen.kt | 2 +- feature/nfc/src/main/res/values/strings.xml | 3 ++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt index 05e9b50f..6520653f 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/AddWifiManuallyDialog.kt @@ -85,6 +85,14 @@ internal fun AddWifiManuallyDialog( } ) + // Show the authentication dropdown. + DropdownView( + items = authListToDisplay(), + label = stringResource(id = R.string.authentication), + placeholder = stringResource(id = R.string.authentication_placeholder), + defaultSelectedItem = authMode + ) { authMode = it } + // Show the password field only if the authentication mode is not open. if (authMode.lowercase() != "open") { // Show the password field. @@ -120,14 +128,6 @@ internal fun AddWifiManuallyDialog( } ) - // Show the authentication dropdown. - DropdownView( - items = authListToDisplay(), - label = stringResource(id = R.string.authentication), - placeholder = stringResource(id = R.string.authentication_placeholder), - defaultSelectedItem = authMode - ) { authMode = it } - // Show the encryption dropdown, only for protected network. if (authMode.lowercase() != "open") { DropdownView( diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt index 94f4433c..46c9bc27 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcPublishScreen.kt @@ -105,7 +105,7 @@ internal fun NfcPublishScreen() { } if (wifiData.macAddress.isNotEmpty()) { NfcTextRow( - title = stringResource(id = R.string.mac_address_label), + title = stringResource(id = R.string.mac_address), text = wifiData.macAddress.uppercase() ) } diff --git a/feature/nfc/src/main/res/values/strings.xml b/feature/nfc/src/main/res/values/strings.xml index 21b2e386..93d62d91 100644 --- a/feature/nfc/src/main/res/values/strings.xml +++ b/feature/nfc/src/main/res/values/strings.xml @@ -34,7 +34,8 @@ SSID Enter SSID SSID is required - MAC address + MAC address + MAC address (optional) Enter MAC address The MAC address consists of hexadecimal digits in the format XX:XX:XX:XX:XX:XX. Encryption From 9d3642bbfe8ae50bc8c815a75f131237e3f5b0bc Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 7 Jun 2024 14:50:44 +0200 Subject: [PATCH 75/78] File rename --- .../android/wifi/provisioner/feature/nfc/NfcDestinations.kt | 4 ++-- .../feature/nfc/view/{NfcScreen.kt => HomeScreen.kt} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/{NfcScreen.kt => HomeScreen.kt} (99%) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/NfcDestinations.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/NfcDestinations.kt index 266348c4..601b62fe 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/NfcDestinations.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/NfcDestinations.kt @@ -6,7 +6,7 @@ import no.nordicsemi.android.common.navigation.createDestination import no.nordicsemi.android.common.navigation.createSimpleDestination import no.nordicsemi.android.common.navigation.defineDestination import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.NfcPublishScreen -import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.NfcScreen +import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.HomeScreen import no.nordicsemi.android.wifi.provisioner.feature.nfc.view.WifiScannerScreen import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData @@ -23,7 +23,7 @@ val NfcPublishDestination = @RequiresApi(Build.VERSION_CODES.M) val NfcProvisionerDestinations = listOf( defineDestination(NfcDestination) { - NfcScreen() + HomeScreen() }, defineDestination(WifiScannerDestination) { WifiScannerScreen() diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/HomeScreen.kt similarity index 99% rename from feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt rename to feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/HomeScreen.kt index cb714579..f97f6c2f 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/NfcScreen.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/HomeScreen.kt @@ -45,7 +45,7 @@ import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnScanClickE @RequiresApi(Build.VERSION_CODES.M) @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun NfcScreen() { +internal fun HomeScreen() { val viewModel: NfcProvisioningViewModel = hiltViewModel() val onEvent: (NfcProvisioningViewEvent) -> Unit = { viewModel.onEvent(it) } val snackbarHostState = remember { SnackbarHostState() } From b5ca75288b3bdf8c447a5d68ffc0589f6f537871 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 7 Jun 2024 15:18:09 +0200 Subject: [PATCH 76/78] Fixed wifi state not updating if disabled --- .../nfc/permission/view/WifiNotAvailableView.kt | 2 +- .../nfc/permission/wifi/WifiStateManager.kt | 10 +++++----- .../feature/nfc/view/WifiScannerView.kt | 14 ++++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiNotAvailableView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiNotAvailableView.kt index 0273abdb..56e8b89e 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiNotAvailableView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/view/WifiNotAvailableView.kt @@ -27,7 +27,7 @@ internal fun WifiNotAvailableView() { @Preview @Composable -private fun BluetoothNotAvailableView_Preview() { +private fun WifiNotAvailableView_Preview() { NordicTheme { WifiNotAvailableView() } diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/wifi/WifiStateManager.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/wifi/WifiStateManager.kt index 93217685..898fe900 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/wifi/WifiStateManager.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/permission/wifi/WifiStateManager.kt @@ -1,11 +1,11 @@ package no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.wifi import android.annotation.SuppressLint -import android.bluetooth.BluetoothAdapter import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.net.wifi.WifiManager import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat.RECEIVER_EXPORTED import dagger.hilt.android.qualifiers.ApplicationContext @@ -30,15 +30,15 @@ class WifiStateManager @Inject constructor( @SuppressLint("WrongConstant") fun wifiState() = callbackFlow { - trySend(getBluetoothPermissionState()) + trySend(getWifiPermissionState()) val wifiStateChangeHandler = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - trySend(getBluetoothPermissionState()) + trySend(getWifiPermissionState()) } } val filter = IntentFilter().apply { - addAction(BluetoothAdapter.ACTION_STATE_CHANGED) + addAction(WifiManager.WIFI_STATE_CHANGED_ACTION) addAction(REFRESH_PERMISSIONS) } @@ -67,7 +67,7 @@ class WifiStateManager @Inject constructor( return utils.isWifiPermissionDeniedForever(context) } - private fun getBluetoothPermissionState() = when { + private fun getWifiPermissionState() = when { !utils.isWifiAvailable -> WifiPermissionState.NotAvailable( WifiPermissionNotAvailableReason.NOT_AVAILABLE ) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index b37982ca..bd9fd2bf 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -71,6 +71,7 @@ import no.nordicsemi.android.wifi.provisioner.nfc.domain.Error import no.nordicsemi.android.wifi.provisioner.nfc.domain.Loading import no.nordicsemi.android.wifi.provisioner.nfc.domain.Success import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeBelowTiramisu +import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiAuthTypeTiramisuOrAbove import no.nordicsemi.android.wifi.provisioner.nfc.domain.WifiData import no.nordicsemi.android.wifi.provisioner.ui.view.WifiSortView @@ -241,7 +242,8 @@ private fun NetworkItem( onEvent: (WifiScannerViewEvent) -> Unit, ) { val securityType = AuthenticationMode.get(network) - val isProtected = securityType.contains(WifiAuthTypeBelowTiramisu.OPEN).not() + val isOpen = securityType.contains(WifiAuthTypeBelowTiramisu.OPEN) or + securityType.contains(WifiAuthTypeTiramisuOrAbove.OPEN) Row( verticalAlignment = Alignment.CenterVertically, @@ -249,10 +251,7 @@ private fun NetworkItem( modifier = modifier .fillMaxWidth() .clickable { - if (isProtected) { - // Show the password dialog - onEvent(OnNetworkSelectEvent(network)) - } else { + if (isOpen) { // Password dialog is not required for open networks. val wifiData = WifiData( ssid = network.SSID, @@ -262,6 +261,9 @@ private fun NetworkItem( encryptionMode = EncryptionMode.NONE ) onEvent(OnPasswordSetEvent(wifiData)) + } else { + // Show the password dialog + onEvent(OnNetworkSelectEvent(network)) } }, ) { @@ -306,7 +308,7 @@ private fun NetworkItem( modifier = Modifier.alpha(0.7f), ) } - if (isProtected) { + if (!isOpen) { // Show the lock icon for protected networks. Icon( imageVector = Icons.Outlined.Lock, From b77d93b8a701d2e639076e9a00dac49afe4c7602 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 7 Jun 2024 15:55:59 +0200 Subject: [PATCH 77/78] Added VerticalBlueBar on expand --- .../nfc/uicomponent/VerticalBlueBar.kt | 43 +++++++++++++++++++ .../feature/nfc/view/WifiScannerView.kt | 34 +++++++-------- 2 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/VerticalBlueBar.kt diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/VerticalBlueBar.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/VerticalBlueBar.kt new file mode 100644 index 00000000..bea0b86b --- /dev/null +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/uicomponent/VerticalBlueBar.kt @@ -0,0 +1,43 @@ +package no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +/** + * This composable is used to create a vertical blue bar with a content. + * + * @param content The content to be displayed in the blue bar. + */ +@Composable +internal fun VerticalBlueBar( + content: @Composable ColumnScope.() -> Unit, +) { + Row( + modifier = Modifier.height(IntrinsicSize.Min).padding(start = 8.dp, top = 8.dp, bottom = 8.dp) + ) { + Box( + modifier = Modifier + .fillMaxHeight() + .width(8.dp) + .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(4.dp)) + ) + Column( + modifier = Modifier.padding(start = 8.dp), + ) { + content() + } + } +} \ No newline at end of file diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt index bd9fd2bf..479de151 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/view/WifiScannerView.kt @@ -58,6 +58,7 @@ import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireLoca import no.nordicsemi.android.wifi.provisioner.feature.nfc.permission.RequireWifi import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.PasswordDialog import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.RssiIconView +import no.nordicsemi.android.wifi.provisioner.feature.nfc.uicomponent.VerticalBlueBar import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnNavigateUpClickEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnNetworkSelectEvent import no.nordicsemi.android.wifi.provisioner.feature.nfc.viewmodel.OnPasswordCancelEvent @@ -111,7 +112,7 @@ internal fun WifiScannerScreen() { .clickable { isGroupedBySsid = !isGroupedBySsid } - .padding(8.dp, 8.dp, 16.dp, 8.dp) + .padding(8.dp) ) } @@ -353,28 +354,27 @@ private fun GroupBySsid( Icon( imageVector = expandIcon, contentDescription = null, - modifier = Modifier.padding(8.dp, 8.dp, 16.dp, 8.dp) + modifier = Modifier.padding(8.dp) ) } // Show networks under the same SSID. AnimatedVisibility(visible = isExpanded) { Column { HorizontalDivider() - network.forEach { scanResult -> - NetworkItem( - network = scanResult, - modifier = Modifier.padding( - start = 16.dp, - top = 8.dp, - end = 16.dp, - bottom = 8.dp - ), - onEvent = onEvent - ) - HorizontalDivider( - modifier = Modifier.padding(start = 16.dp), - thickness = 0.5.dp - ) + VerticalBlueBar { + network.forEach { scanResult -> + NetworkItem( + network = scanResult, + modifier = Modifier.padding(8.dp), + onEvent = onEvent + ) + if (scanResult != network.last()) { + HorizontalDivider( + modifier = Modifier.padding(start = 16.dp), + thickness = 0.5.dp + ) + } + } } } } From 1a4af765abb3a933242045038428f702b3247eb8 Mon Sep 17 00:00:00 2001 From: hiar Date: Fri, 7 Jun 2024 15:56:20 +0200 Subject: [PATCH 78/78] Changed delay to 5 seconds --- .../provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt index 41ce92e1..3b8f69a9 100644 --- a/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt +++ b/feature/nfc/src/main/java/no/nordicsemi/android/wifi/provisioner/feature/nfc/viewmodel/WifiScannerViewModel.kt @@ -100,7 +100,7 @@ internal class WifiScannerViewModel @Inject constructor( _viewState.value = _viewState.value.copy(networks = scanResults) }.launchIn(viewModelScope) viewModelScope.launch { - delay(2.seconds) + delay(5.seconds) _viewState.value = _viewState.value.copy(isRefreshing = false) } } catch (e: Exception) {