Implement preconditions for app usage (BLE support, permissions, bluetooth and network condition)

This commit is contained in:
2026-06-25 21:58:17 +07:00
parent cc17e03ec1
commit 6c6a5bfff9
27 changed files with 336 additions and 166 deletions
+1 -1
View File
@@ -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 {
+11 -5
View File
@@ -1,15 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<!-- REQUIRED BLE FOR APP -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Android 6.011 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Android 12+ -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"
tools:targetApi="31" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="31"
/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" tools:targetApi="31"/>
<!-- Allow access http: android:usesCleartextTraffic="true" -->
@@ -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()
@@ -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<NetworkMonitor> { AndroidNetworkMonitor(androidContext()) }
}
@@ -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
@@ -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<NetworkStatus> = _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)
}
}
@@ -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)
}
@@ -72,4 +72,6 @@
<string name="location_disabled_title">LOCATION DISABLED</string>
<string name="location_disabled_info">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.</string>
<string name="network_not_available_title">NO NETWORK CONNECTION</string>
<string name="network_not_available_info">Please check Wi-Fi or mobile data settings to continue.</string>
</resources>
@@ -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 = {
@@ -1,9 +0,0 @@
package com.digitoolsolutions.app.torquevaultkmp
class Greeting {
private val platform = getPlatform()
fun greet(): String {
return "Hello, ${platform.name}!"
}
}
@@ -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 = {
@@ -23,7 +23,7 @@ class AuthViewModel(
private val _loginState = MutableStateFlow<LoginUiState>(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) {
@@ -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)
}
}
}
@@ -66,7 +66,7 @@ fun UartCommunicationScreen(
Icon(
Icons.Rounded.DeleteForever,
contentDescription = "Clear history",
tint = Color.Red
tint = Color.White
)
}
}
@@ -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 -> {}
}
}
}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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())
)
}
@@ -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<NetworkStatus>
fun start()
fun stop()
}
@@ -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<Boolean>
fun enableLocation()
fun checkLocationState()
@@ -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()
@@ -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<NetworkMonitor> { IOSNetworkMonitor() }
}
@@ -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<NetworkStatus> = _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)
}
}
@@ -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<Boolean> = _isLocationEnabled.asStateFlow()
override fun enableLocation() { /* no-op */ }
override fun checkLocationState() { /* no-op */ }
override val hasBleFeature: Boolean = true
}