diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index b95354f..87d38eb 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -89,7 +89,7 @@ android { minSdk = libs.versions.android.minSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt() versionCode = 2 - versionName = "1.0.1-2" + versionName = "1.0.2" } packaging { resources { diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 8653bd0..4e2854e 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -1,15 +1,21 @@ + + + + - + + - - - + diff --git a/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Platform.android.kt b/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Platform.android.kt index 872cda2..65c48c5 100644 --- a/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Platform.android.kt +++ b/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Platform.android.kt @@ -3,7 +3,7 @@ package com.digitoolsolutions.app.torquevaultkmp import android.os.Build class AndroidPlatform : Platform { - override val name: String = "Android ${Build.VERSION.SDK_INT}" + override val name: String = "Android" } actual fun getPlatform(): Platform = AndroidPlatform() \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/PlatformModules.kt b/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/PlatformModules.kt index d7f81aa..a061f28 100644 --- a/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/PlatformModules.kt +++ b/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/PlatformModules.kt @@ -6,7 +6,9 @@ import com.digitoolsolutions.app.torquevaultkmp.AppStoragePlatform import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabase import com.digitoolsolutions.app.torquevaultkmp.utils.AndroidBluetoothManager +import com.digitoolsolutions.app.torquevaultkmp.utils.AndroidNetworkMonitor import com.digitoolsolutions.app.torquevaultkmp.utils.BluetoothManager +import com.digitoolsolutions.app.torquevaultkmp.utils.NetworkMonitor import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandlerPlatform import org.koin.android.ext.koin.androidContext @@ -23,4 +25,5 @@ actual val platformModule = module { name = "app.db" ) } + single { AndroidNetworkMonitor(androidContext()) } } diff --git a/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.android.kt b/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.android.kt index ed80090..17ac3d8 100644 --- a/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.android.kt +++ b/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.android.kt @@ -50,11 +50,13 @@ class AndroidBluetoothManager(private val context: Context) : BluetoothManager { override fun checkBluetoothPermission() { val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + /* For android 12+ */ listOf( Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT ) } else { + /* For android 11- */ listOf( Manifest.permission.BLUETOOTH, Manifest.permission.ACCESS_FINE_LOCATION diff --git a/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NetworkMonitor.android.kt b/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NetworkMonitor.android.kt new file mode 100644 index 0000000..12a87f0 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NetworkMonitor.android.kt @@ -0,0 +1,34 @@ +package com.digitoolsolutions.app.torquevaultkmp.utils + +import android.content.Context +import android.net.ConnectivityManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import kotlinx.coroutines.flow.asStateFlow + +class AndroidNetworkMonitor(private val context: Context): NetworkMonitor { + private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + private val _status = MutableStateFlow(NetworkStatus.Unavailable) + override val status: StateFlow = _status.asStateFlow() + private val callback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + _status.value = NetworkStatus.Available + } + override fun onLost(network: Network) { + _status.value = NetworkStatus.Unavailable + } + } + override fun start() { + val request = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + connectivityManager.registerNetworkCallback(request, callback) + } + + override fun stop() { + connectivityManager.unregisterNetworkCallback(callback) + } +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.android.kt b/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.android.kt index d7f1139..467779e 100644 --- a/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.android.kt +++ b/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.android.kt @@ -7,6 +7,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.PackageManager import android.location.LocationManager import android.os.Build import android.provider.Settings @@ -23,7 +24,7 @@ class PermissionHandlerPlatform(private val context: Context): PermissionHandler @SuppressLint("ContextCastToActivity") @Composable override fun HandlePermissionRequest ( - onPermissionResult: (Boolean, Boolean) -> Unit, + onPermissionResult: (Boolean) -> Unit, content: @Composable (requestPermissions: () -> Unit) -> Unit ) { val permissions = if (isPreAndroid12()) { @@ -43,7 +44,7 @@ class PermissionHandlerPlatform(private val context: Context): PermissionHandler val launcher = rememberLauncherForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { result -> - val allGranted = result.values.all { it } +// val allGranted = result.values.all { it } val permanentlyDenied = if (isPreAndroid12()) { /** For android 6.0 - 11 */ val deniedLocation = result[Manifest.permission.ACCESS_FINE_LOCATION] == false @@ -67,7 +68,7 @@ class PermissionHandlerPlatform(private val context: Context): PermissionHandler } ?: false permDeniedScan || permDeniedConnect } - onPermissionResult(allGranted, permanentlyDenied) + onPermissionResult(permanentlyDenied) } content { launcher.launch(permissions) @@ -111,4 +112,5 @@ class PermissionHandlerPlatform(private val context: Context): PermissionHandler } context.startActivity(intent) } + override val hasBleFeature: Boolean = context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) } \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 2fc5c9c..977901c 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -72,4 +72,6 @@ LOCATION DISABLED Location is turned off. Most Android phones require it in order to scan for Bluetooth LE devices.\n Click the button below to enable Location. + NO NETWORK CONNECTION + Please check Wi-Fi or mobile data settings to continue. diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/App.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/App.kt index 01ddf40..6333e78 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/App.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/App.kt @@ -4,22 +4,26 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import com.digitoolsolutions.app.torquevaultkmp.screens.MainScreen -import com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.BluetoothRequirementWrapper +import com.digitoolsolutions.app.torquevaultkmp.screens.requirement.RequirementWrapper +import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerViewModel import com.digitoolsolutions.app.torquevaultkmp.theme.TorqueVaultTheme -import io.github.aakira.napier.Napier import org.koin.compose.viewmodel.koinViewModel @Composable @Preview fun App( - viewModel: AppViewModel = koinViewModel() + viewModel: AppViewModel = koinViewModel(), + scannerViewModel: ScannerViewModel = koinViewModel(), ) { val savedTheme = viewModel.isDarkTheme.value val isDarkTheme = savedTheme ?: isSystemInDarkTheme() val isLoggedIn = viewModel.isLoggedIn() TorqueVaultTheme(darkTheme = isDarkTheme) { - BluetoothRequirementWrapper { + RequirementWrapper( + onReady = { scannerViewModel.startScan() }, + onPause = { scannerViewModel.stopScan() } + ) { MainScreen( isDarkTheme = isDarkTheme, onThemeToggle = { diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Greeting.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Greeting.kt deleted file mode 100644 index 5ca24fa..0000000 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Greeting.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.digitoolsolutions.app.torquevaultkmp - -class Greeting { - private val platform = getPlatform() - - fun greet(): String { - return "Hello, ${platform.name}!" - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt index 5ae598b..d65f463 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt @@ -41,6 +41,7 @@ import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingDestinat import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingScreen import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel import torquevaultkmp.composeapp.generated.resources.Res import torquevaultkmp.composeapp.generated.resources.btn_login @@ -52,8 +53,7 @@ fun MainScreen( isDarkTheme: Boolean, onThemeToggle: (Boolean) -> Unit, isLoggedIn: Boolean, - viewModel: AppViewModel = koinViewModel(), - scannerViewModel: ScannerViewModel = koinViewModel(), + viewModel: AppViewModel = koinInject() ) { val destinations = listOf( HomeDestination, @@ -88,10 +88,6 @@ fun MainScreen( ) } - LaunchedEffect(Unit) { - scannerViewModel.startScan() - } - Scaffold( modifier = Modifier.fillMaxSize(), bottomBar = { diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/AuthViewModel.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/AuthViewModel.kt index f3b0132..8aafa3d 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/AuthViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/AuthViewModel.kt @@ -23,7 +23,7 @@ class AuthViewModel( private val _loginState = MutableStateFlow(LoginUiState.Idle) val loginState = _loginState.asStateFlow() - private val _serverUrl = MutableStateFlow(tokenManager.getBaseUrl() ?: "") + private val _serverUrl = MutableStateFlow(tokenManager.getBaseUrl() ?: "http://digitoolsolutions.synology.me:3000") val serverUrl = _serverUrl.asStateFlow() fun onChangeServerUrl(url: String) { diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/BluetoothRequirementWrapper.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/BluetoothRequirementWrapper.kt deleted file mode 100644 index ac5f7ce..0000000 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/BluetoothRequirementWrapper.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -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.unit.dp -import androidx.lifecycle.compose.LifecycleResumeEffect -import com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.views.BluetoothDisabledView -import com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.views.LocationDisabledView -import com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.views.LocationPermissionRequiredView -import com.digitoolsolutions.app.torquevaultkmp.utils.BluetoothManager -import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler -import org.koin.compose.koinInject - -@Composable -fun BluetoothRequirementWrapper( - content: @Composable () -> Unit, -) { - val bluetoothManager: BluetoothManager = koinInject() - val permissionHandler: PermissionHandler = koinInject() - val isEnabled by bluetoothManager.isBluetoothEnabled.collectAsState() - val hasPermission by bluetoothManager.hasBluetoothPermission.collectAsState() - var hasPermanentlyDenied by remember { mutableStateOf(false) } - val isLocationEnabled by permissionHandler.isLocationEnabled.collectAsState() - var showPermissionRequest by remember { mutableStateOf(false) } - - if (showPermissionRequest) { - permissionHandler.HandlePermissionRequest( - { allGranted, denied -> - showPermissionRequest = false - bluetoothManager.checkBluetoothPermission() - hasPermanentlyDenied = denied - } - ) { - requestPermissions -> - LaunchedEffect(Unit) { - requestPermissions() - } - } - } - - LifecycleResumeEffect(Unit) { - bluetoothManager.checkBluetoothState() - bluetoothManager.checkBluetoothPermission() - permissionHandler.checkLocationState() - onPauseOrDispose { } - } - - /* MainScreen */ - content() - /* Overlay content */ - if (!isEnabled || !hasPermission || !isLocationEnabled) { - BluetoothRequestScreen( - isEnabled = isEnabled, - hasPermission = hasPermission, - hasPermanentlyDenied = hasPermanentlyDenied, - isLocationEnabled = isLocationEnabled, - onEnableBluetooth = { bluetoothManager.enableBluetooth() }, - onEnableLocation = { permissionHandler.enableLocation() }, - onRequestPermission = { showPermissionRequest = true }, - onOpenSettings = { bluetoothManager.openSettings() } - ) - } -} - -@Composable -fun BluetoothRequestScreen( - isEnabled: Boolean, - hasPermission: Boolean, - hasPermanentlyDenied: Boolean, - isLocationEnabled: Boolean, - onEnableBluetooth: () -> Unit, - onEnableLocation: () -> Unit, - onRequestPermission: () -> Unit, - onOpenSettings: () -> Unit -) { - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(12.dp) - ){ - when { - !isEnabled -> BluetoothDisabledView(onEnable = onEnableBluetooth) - !hasPermission -> LocationPermissionRequiredView( - hasPermission = hasPermission, - hasPermanentlyDenied = hasPermanentlyDenied, - onRequestPermission = onRequestPermission, - onOpenSetting = onOpenSettings - ) - !isLocationEnabled -> LocationDisabledView(onEnable = onEnableLocation) - } - } -} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/communication/UartCommunicationScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/communication/UartCommunicationScreen.kt index c6cd4d8..343246d 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/communication/UartCommunicationScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/communication/UartCommunicationScreen.kt @@ -66,7 +66,7 @@ fun UartCommunicationScreen( Icon( Icons.Rounded.DeleteForever, contentDescription = "Clear history", - tint = Color.Red + tint = Color.White ) } } diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/RequirementWrapper.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/RequirementWrapper.kt new file mode 100644 index 0000000..58f42cd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/RequirementWrapper.kt @@ -0,0 +1,180 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.requirement + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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.unit.dp +import androidx.lifecycle.compose.LifecycleResumeEffect +import com.digitoolsolutions.app.torquevaultkmp.getPlatform +import com.digitoolsolutions.app.torquevaultkmp.screens.requirement.views.BluetoothDisabledView +import com.digitoolsolutions.app.torquevaultkmp.screens.requirement.views.BluetoothNotAvailableView +import com.digitoolsolutions.app.torquevaultkmp.screens.requirement.views.BluetoothPermissionRequiredView +import com.digitoolsolutions.app.torquevaultkmp.screens.requirement.views.LocationDisabledView +import com.digitoolsolutions.app.torquevaultkmp.screens.requirement.views.LocationPermissionRequiredView +import com.digitoolsolutions.app.torquevaultkmp.screens.requirement.views.NetworkNotAvailableView +import com.digitoolsolutions.app.torquevaultkmp.utils.BluetoothManager +import com.digitoolsolutions.app.torquevaultkmp.utils.NetworkMonitor +import com.digitoolsolutions.app.torquevaultkmp.utils.NetworkStatus +import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler +import org.koin.compose.koinInject + +data class Requirements( + val hasBleFeature: Boolean, + val isEnabled: Boolean, + val hasPermission: Boolean, + val hasPermanentlyDenied: Boolean, + val isLocationEnabled: Boolean, + val networkAvailable: Boolean, +) + +sealed class RequirementState { + object BleUnsupported : RequirementState() + object BluetoothDisabled : RequirementState() + data class BluetoothPermissionRequired(val permanentlyDenied: Boolean) : RequirementState() + data class LocationPermissionRequired(val permanentlyDenied: Boolean) : RequirementState() + object LocationDisabled : RequirementState() + object NetworkUnavailable : RequirementState() + object Ready : RequirementState() +} + +fun Requirements.toState(): RequirementState { + return when { + !hasBleFeature -> RequirementState.BleUnsupported + !hasPermission -> { + if (getPlatform().name == "IOS") { + RequirementState.BluetoothPermissionRequired(hasPermanentlyDenied) + } else { + RequirementState.LocationPermissionRequired(hasPermanentlyDenied) + } + } + !isEnabled -> RequirementState.BluetoothDisabled + !isLocationEnabled -> RequirementState.LocationDisabled + !networkAvailable -> RequirementState.NetworkUnavailable + else -> RequirementState.Ready + } +} +@Composable +fun RequirementWrapper( + onReady: () -> Unit, + onPause: () -> Unit, + content: @Composable () -> Unit, +) { + val bluetoothManager: BluetoothManager = koinInject() + val permissionHandler: PermissionHandler = koinInject() + val networkMonitor: NetworkMonitor = koinInject() + val hasBleFeature = permissionHandler.hasBleFeature + val isEnabled by bluetoothManager.isBluetoothEnabled.collectAsState() + val hasPermission by bluetoothManager.hasBluetoothPermission.collectAsState() + var hasPermanentlyDenied by remember { mutableStateOf(false) } + val isLocationEnabled by permissionHandler.isLocationEnabled.collectAsState() + var showPermissionRequest by remember { mutableStateOf(false) } + val networkStatus by networkMonitor.status.collectAsState() + + if (showPermissionRequest) { + permissionHandler.HandlePermissionRequest( + { denied -> + showPermissionRequest = false + bluetoothManager.checkBluetoothPermission() + hasPermanentlyDenied = denied + } + ) { + requestPermissions -> + LaunchedEffect(Unit) { + requestPermissions() + } + } + } + + LifecycleResumeEffect(Unit) { + bluetoothManager.checkBluetoothState() + bluetoothManager.checkBluetoothPermission() + permissionHandler.checkLocationState() + onPauseOrDispose { } + } + + DisposableEffect(networkMonitor) { + networkMonitor.start() + onDispose { + networkMonitor.stop() + } + } + + val requirements = Requirements( + isEnabled = isEnabled, + hasPermission = hasPermission, + hasPermanentlyDenied = hasPermanentlyDenied, + isLocationEnabled = isLocationEnabled, + hasBleFeature = hasBleFeature, + networkAvailable = networkStatus == NetworkStatus.Available + ) + + val state = requirements.toState() + LaunchedEffect(state) { + if (state == RequirementState.Ready) { + onReady() + } else { + onPause() + } + } + + /* MainScreen */ + content() + /* Overlay content */ + if (state != RequirementState.Ready) { + RequirementContent( + state = state, + onEnableBluetooth = { bluetoothManager.enableBluetooth() }, + onEnableLocation = { permissionHandler.enableLocation() }, + onRequestPermission = { showPermissionRequest = true }, + onOpenSettings = { bluetoothManager.openSettings() } + ) + } +} + +@Composable +fun RequirementContent( + state: RequirementState, + onEnableBluetooth: () -> Unit, + onEnableLocation: () -> Unit, + onRequestPermission: () -> Unit, + onOpenSettings: () -> Unit +) { + val platform = getPlatform().name + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(12.dp) + ){ + when (state){ + RequirementState.BleUnsupported -> BluetoothNotAvailableView() + RequirementState.BluetoothDisabled -> BluetoothDisabledView(onEnable = onEnableBluetooth) + is RequirementState.LocationPermissionRequired -> LocationPermissionRequiredView( + hasPermission = false, + hasPermanentlyDenied = state.permanentlyDenied, + onRequestPermission = onRequestPermission, + onOpenSetting = onOpenSettings + ) + is RequirementState.BluetoothPermissionRequired -> BluetoothPermissionRequiredView( + hasPermission = false, + hasPermanentlyDenied = state.permanentlyDenied, + onRequestPermission = onRequestPermission, + onOpenSetting = onOpenSettings + ) + RequirementState.LocationDisabled -> LocationDisabledView(onEnable = onEnableLocation) + RequirementState.NetworkUnavailable -> NetworkNotAvailableView() + RequirementState.Ready -> {} + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/BluetoothDisabledView.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/BluetoothDisabledView.kt similarity index 89% rename from composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/BluetoothDisabledView.kt rename to composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/BluetoothDisabledView.kt index 5969ad5..2b39dfa 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/BluetoothDisabledView.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/BluetoothDisabledView.kt @@ -1,7 +1,6 @@ -package com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.views +package com.digitoolsolutions.app.torquevaultkmp.screens.requirement.views 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 @@ -10,7 +9,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import com.digitoolsolutions.app.torquevaultkmp.components.WarningView import org.jetbrains.compose.resources.stringResource import torquevaultkmp.composeapp.generated.resources.Res diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/BluetoothNotAvailableView.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/BluetoothNotAvailableView.kt similarity index 93% rename from composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/BluetoothNotAvailableView.kt rename to composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/BluetoothNotAvailableView.kt index d5f538b..1c708c6 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/BluetoothNotAvailableView.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/BluetoothNotAvailableView.kt @@ -1,4 +1,4 @@ -package com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.views +package com.digitoolsolutions.app.torquevaultkmp.screens.requirement.views import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.rememberScrollState diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/BluetoothPermissionRequiredView.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/BluetoothPermissionRequiredView.kt similarity index 71% rename from composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/BluetoothPermissionRequiredView.kt rename to composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/BluetoothPermissionRequiredView.kt index cce8201..3be9cd2 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/BluetoothPermissionRequiredView.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/BluetoothPermissionRequiredView.kt @@ -1,30 +1,15 @@ -package com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.views +package com.digitoolsolutions.app.torquevaultkmp.screens.requirement.views -//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.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.rounded.BluetoothDisabled import androidx.compose.material3.Button -import androidx.compose.material3.SegmentedButtonDefaults.Icon 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.platform.LocalContext import com.digitoolsolutions.app.torquevaultkmp.components.WarningView -import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import torquevaultkmp.composeapp.generated.resources.Res import torquevaultkmp.composeapp.generated.resources.action_grant_permission diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/LocationDisabledView.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/LocationDisabledView.kt similarity index 94% rename from composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/LocationDisabledView.kt rename to composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/LocationDisabledView.kt index b881a3d..f253b45 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/LocationDisabledView.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/LocationDisabledView.kt @@ -1,4 +1,4 @@ -package com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.views +package com.digitoolsolutions.app.torquevaultkmp.screens.requirement.views import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.rememberScrollState diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/LocationPermissionRequiredView.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/LocationPermissionRequiredView.kt similarity index 88% rename from composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/LocationPermissionRequiredView.kt rename to composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/LocationPermissionRequiredView.kt index 018f3a9..892bed8 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bluetooth/views/LocationPermissionRequiredView.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/LocationPermissionRequiredView.kt @@ -1,18 +1,14 @@ -package com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.views +package com.digitoolsolutions.app.torquevaultkmp.screens.requirement.views -import androidx.compose.foundation.background 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.rounded.LocationOff import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import com.digitoolsolutions.app.torquevaultkmp.components.WarningView import org.jetbrains.compose.resources.stringResource import torquevaultkmp.composeapp.generated.resources.Res diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/NetworkNotAvailableView.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/NetworkNotAvailableView.kt new file mode 100644 index 0000000..bf2caad --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/views/NetworkNotAvailableView.kt @@ -0,0 +1,26 @@ +package com.digitoolsolutions.app.torquevaultkmp.screens.requirement.views + +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.rounded.WifiOff +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.digitoolsolutions.app.torquevaultkmp.components.WarningView +import org.jetbrains.compose.resources.stringResource +import torquevaultkmp.composeapp.generated.resources.Res +import torquevaultkmp.composeapp.generated.resources.network_not_available_info +import torquevaultkmp.composeapp.generated.resources.network_not_available_title + +@Composable +internal fun NetworkNotAvailableView() { + WarningView( + imageVector = Icons.Rounded.WifiOff, + title = stringResource(Res.string.network_not_available_title), + hint = stringResource(Res.string.network_not_available_info), + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NetworkMonitor.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NetworkMonitor.kt new file mode 100644 index 0000000..824e9eb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NetworkMonitor.kt @@ -0,0 +1,13 @@ +package com.digitoolsolutions.app.torquevaultkmp.utils + +import kotlinx.coroutines.flow.StateFlow + +enum class NetworkStatus { + Available, Unavailable +} + +interface NetworkMonitor { + val status: StateFlow + fun start() + fun stop() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.kt index 385f995..9b3df51 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.kt @@ -6,9 +6,10 @@ import kotlinx.coroutines.flow.StateFlow interface PermissionHandler { @Composable fun HandlePermissionRequest( - onPermissionResult: (Boolean, Boolean) -> Unit, + onPermissionResult: (Boolean) -> Unit, content: @Composable (requestPermissions: () -> Unit) -> Unit ) + val hasBleFeature: Boolean val isLocationEnabled: StateFlow fun enableLocation() fun checkLocationState() diff --git a/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Platform.ios.kt b/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Platform.ios.kt index 54f066a..cfa9170 100644 --- a/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Platform.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Platform.ios.kt @@ -3,7 +3,7 @@ package com.digitoolsolutions.app.torquevaultkmp import platform.UIKit.UIDevice class IOSPlatform: Platform { - override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion + override val name: String = "IOS" } actual fun getPlatform(): Platform = IOSPlatform() \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/PlatformModules.kt b/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/PlatformModules.kt index ac4c7f6..f21a59c 100644 --- a/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/PlatformModules.kt +++ b/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/PlatformModules.kt @@ -6,7 +6,9 @@ import com.digitoolsolutions.app.torquevaultkmp.AppStoragePlatform import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabase import com.digitoolsolutions.app.torquevaultkmp.utils.BluetoothManager +import com.digitoolsolutions.app.torquevaultkmp.utils.IOSNetworkMonitor import com.digitoolsolutions.app.torquevaultkmp.utils.IosBluetoothManager +import com.digitoolsolutions.app.torquevaultkmp.utils.NetworkMonitor import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandlerPlatform import org.koin.dsl.module @@ -21,4 +23,5 @@ actual val platformModule = module { name = "app.db" ) } + single { IOSNetworkMonitor() } } diff --git a/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NetworkMonitor.ios.kt b/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NetworkMonitor.ios.kt new file mode 100644 index 0000000..39c264d --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NetworkMonitor.ios.kt @@ -0,0 +1,32 @@ +package com.digitoolsolutions.app.torquevaultkmp.utils + +import platform.Network.nw_path_monitor_create +import platform.Network.nw_path_monitor_set_queue +import platform.Network.nw_path_monitor_set_update_handler +import platform.Network.nw_path_monitor_start +import platform.Network.nw_path_monitor_cancel +import platform.Network.nw_path_get_status +import platform.Network.nw_path_status_satisfied +import platform.darwin.dispatch_get_main_queue +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class IOSNetworkMonitor: NetworkMonitor { + private val monitor = nw_path_monitor_create() + private val _status = MutableStateFlow(NetworkStatus.Unavailable) + override val status: StateFlow = _status.asStateFlow() + + override fun start() { + nw_path_monitor_set_update_handler(monitor) { path -> + val isAvailable = nw_path_get_status(path) == nw_path_status_satisfied + _status.value = if (isAvailable) NetworkStatus.Available else NetworkStatus.Unavailable + } + nw_path_monitor_set_queue(monitor, dispatch_get_main_queue()) + nw_path_monitor_start(monitor) + } + + override fun stop() { + nw_path_monitor_cancel(monitor) + } +} diff --git a/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.ios.kt b/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.ios.kt index ba0ecc0..3b40ccb 100644 --- a/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.ios.kt @@ -10,7 +10,7 @@ import org.koin.compose.koinInject class PermissionHandlerPlatform: PermissionHandler { @Composable override fun HandlePermissionRequest( - onPermissionResult: (Boolean, Boolean) -> Unit, + onPermissionResult: (Boolean) -> Unit, content: @Composable (requestPermissions: () -> Unit) -> Unit ) { val bluetoothManager: BluetoothManager = koinInject() @@ -24,12 +24,12 @@ class PermissionHandlerPlatform: PermissionHandler { // will update asynchronously via the CBCentralManagerDelegate val granted = bluetoothManager.hasBluetoothPermission.value - val denied = !granted - onPermissionResult(granted, denied) + onPermissionResult(granted) } } private val _isLocationEnabled = MutableStateFlow(true) override val isLocationEnabled: StateFlow = _isLocationEnabled.asStateFlow() override fun enableLocation() { /* no-op */ } override fun checkLocationState() { /* no-op */ } + override val hasBleFeature: Boolean = true } \ No newline at end of file