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() minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 2 versionCode = 2
versionName = "1.0.1-2" versionName = "1.0.2"
} }
packaging { packaging {
resources { resources {
+11 -5
View File
@@ -1,15 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools" <manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"> 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.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Android 6.011 --> <!-- 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_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Android 12+ --> <!-- Android 12+ -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" android:name="android.permission.BLUETOOTH_SCAN"
tools:targetApi="31" /> android:usesPermissionFlags="neverForLocation"
tools:targetApi="31"
/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" tools:targetApi="31"/> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" tools:targetApi="31"/>
<!-- Allow access http: android:usesCleartextTraffic="true" --> <!-- Allow access http: android:usesCleartextTraffic="true" -->
@@ -3,7 +3,7 @@ package com.digitoolsolutions.app.torquevaultkmp
import android.os.Build import android.os.Build
class AndroidPlatform : Platform { class AndroidPlatform : Platform {
override val name: String = "Android ${Build.VERSION.SDK_INT}" override val name: String = "Android"
} }
actual fun getPlatform(): Platform = AndroidPlatform() 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.AppStorage
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabase import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabase
import com.digitoolsolutions.app.torquevaultkmp.utils.AndroidBluetoothManager 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.BluetoothManager
import com.digitoolsolutions.app.torquevaultkmp.utils.NetworkMonitor
import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler
import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandlerPlatform import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandlerPlatform
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
@@ -23,4 +25,5 @@ actual val platformModule = module {
name = "app.db" name = "app.db"
) )
} }
single<NetworkMonitor> { AndroidNetworkMonitor(androidContext()) }
} }
@@ -50,11 +50,13 @@ class AndroidBluetoothManager(private val context: Context) : BluetoothManager {
override fun checkBluetoothPermission() { override fun checkBluetoothPermission() {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
/* For android 12+ */
listOf( listOf(
Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT Manifest.permission.BLUETOOTH_CONNECT
) )
} else { } else {
/* For android 11- */
listOf( listOf(
Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH,
Manifest.permission.ACCESS_FINE_LOCATION 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.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager
import android.location.LocationManager import android.location.LocationManager
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
@@ -23,7 +24,7 @@ class PermissionHandlerPlatform(private val context: Context): PermissionHandler
@SuppressLint("ContextCastToActivity") @SuppressLint("ContextCastToActivity")
@Composable @Composable
override fun HandlePermissionRequest ( override fun HandlePermissionRequest (
onPermissionResult: (Boolean, Boolean) -> Unit, onPermissionResult: (Boolean) -> Unit,
content: @Composable (requestPermissions: () -> Unit) -> Unit content: @Composable (requestPermissions: () -> Unit) -> Unit
) { ) {
val permissions = if (isPreAndroid12()) { val permissions = if (isPreAndroid12()) {
@@ -43,7 +44,7 @@ class PermissionHandlerPlatform(private val context: Context): PermissionHandler
val launcher = rememberLauncherForActivityResult( val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions() ActivityResultContracts.RequestMultiplePermissions()
) { result -> ) { result ->
val allGranted = result.values.all { it } // val allGranted = result.values.all { it }
val permanentlyDenied = if (isPreAndroid12()) { val permanentlyDenied = if (isPreAndroid12()) {
/** For android 6.0 - 11 */ /** For android 6.0 - 11 */
val deniedLocation = result[Manifest.permission.ACCESS_FINE_LOCATION] == false val deniedLocation = result[Manifest.permission.ACCESS_FINE_LOCATION] == false
@@ -67,7 +68,7 @@ class PermissionHandlerPlatform(private val context: Context): PermissionHandler
} ?: false } ?: false
permDeniedScan || permDeniedConnect permDeniedScan || permDeniedConnect
} }
onPermissionResult(allGranted, permanentlyDenied) onPermissionResult(permanentlyDenied)
} }
content { content {
launcher.launch(permissions) launcher.launch(permissions)
@@ -111,4 +112,5 @@ class PermissionHandlerPlatform(private val context: Context): PermissionHandler
} }
context.startActivity(intent) 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_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 <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> 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> </resources>
@@ -4,22 +4,26 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.digitoolsolutions.app.torquevaultkmp.screens.MainScreen 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 com.digitoolsolutions.app.torquevaultkmp.theme.TorqueVaultTheme
import io.github.aakira.napier.Napier
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
@Composable @Composable
@Preview @Preview
fun App( fun App(
viewModel: AppViewModel = koinViewModel() viewModel: AppViewModel = koinViewModel(),
scannerViewModel: ScannerViewModel = koinViewModel(),
) { ) {
val savedTheme = viewModel.isDarkTheme.value val savedTheme = viewModel.isDarkTheme.value
val isDarkTheme = savedTheme ?: isSystemInDarkTheme() val isDarkTheme = savedTheme ?: isSystemInDarkTheme()
val isLoggedIn = viewModel.isLoggedIn() val isLoggedIn = viewModel.isLoggedIn()
TorqueVaultTheme(darkTheme = isDarkTheme) { TorqueVaultTheme(darkTheme = isDarkTheme) {
BluetoothRequirementWrapper { RequirementWrapper(
onReady = { scannerViewModel.startScan() },
onPause = { scannerViewModel.stopScan() }
) {
MainScreen( MainScreen(
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
onThemeToggle = { 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 com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingScreen
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
import torquevaultkmp.composeapp.generated.resources.Res import torquevaultkmp.composeapp.generated.resources.Res
import torquevaultkmp.composeapp.generated.resources.btn_login import torquevaultkmp.composeapp.generated.resources.btn_login
@@ -52,8 +53,7 @@ fun MainScreen(
isDarkTheme: Boolean, isDarkTheme: Boolean,
onThemeToggle: (Boolean) -> Unit, onThemeToggle: (Boolean) -> Unit,
isLoggedIn: Boolean, isLoggedIn: Boolean,
viewModel: AppViewModel = koinViewModel(), viewModel: AppViewModel = koinInject()
scannerViewModel: ScannerViewModel = koinViewModel(),
) { ) {
val destinations = listOf( val destinations = listOf(
HomeDestination, HomeDestination,
@@ -88,10 +88,6 @@ fun MainScreen(
) )
} }
LaunchedEffect(Unit) {
scannerViewModel.startScan()
}
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomBar = { bottomBar = {
@@ -23,7 +23,7 @@ class AuthViewModel(
private val _loginState = MutableStateFlow<LoginUiState>(LoginUiState.Idle) private val _loginState = MutableStateFlow<LoginUiState>(LoginUiState.Idle)
val loginState = _loginState.asStateFlow() 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() val serverUrl = _serverUrl.asStateFlow()
fun onChangeServerUrl(url: String) { 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( Icon(
Icons.Rounded.DeleteForever, Icons.Rounded.DeleteForever,
contentDescription = "Clear history", 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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -10,7 +9,6 @@ import androidx.compose.material3.Button
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.digitoolsolutions.app.torquevaultkmp.components.WarningView import com.digitoolsolutions.app.torquevaultkmp.components.WarningView
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import torquevaultkmp.composeapp.generated.resources.Res 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.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState 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.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.BluetoothDisabled import androidx.compose.material.icons.rounded.BluetoothDisabled
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.SegmentedButtonDefaults.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Modifier
//import androidx.compose.ui.platform.LocalContext
import com.digitoolsolutions.app.torquevaultkmp.components.WarningView import com.digitoolsolutions.app.torquevaultkmp.components.WarningView
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import torquevaultkmp.composeapp.generated.resources.Res import torquevaultkmp.composeapp.generated.resources.Res
import torquevaultkmp.composeapp.generated.resources.action_grant_permission 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.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState 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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.LocationOff import androidx.compose.material.icons.rounded.LocationOff
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.digitoolsolutions.app.torquevaultkmp.components.WarningView import com.digitoolsolutions.app.torquevaultkmp.components.WarningView
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import torquevaultkmp.composeapp.generated.resources.Res 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 { interface PermissionHandler {
@Composable @Composable
fun HandlePermissionRequest( fun HandlePermissionRequest(
onPermissionResult: (Boolean, Boolean) -> Unit, onPermissionResult: (Boolean) -> Unit,
content: @Composable (requestPermissions: () -> Unit) -> Unit content: @Composable (requestPermissions: () -> Unit) -> Unit
) )
val hasBleFeature: Boolean
val isLocationEnabled: StateFlow<Boolean> val isLocationEnabled: StateFlow<Boolean>
fun enableLocation() fun enableLocation()
fun checkLocationState() fun checkLocationState()
@@ -3,7 +3,7 @@ package com.digitoolsolutions.app.torquevaultkmp
import platform.UIKit.UIDevice import platform.UIKit.UIDevice
class IOSPlatform: Platform { class IOSPlatform: Platform {
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion override val name: String = "IOS"
} }
actual fun getPlatform(): Platform = IOSPlatform() 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.AppStorage
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabase import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabase
import com.digitoolsolutions.app.torquevaultkmp.utils.BluetoothManager 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.IosBluetoothManager
import com.digitoolsolutions.app.torquevaultkmp.utils.NetworkMonitor
import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler
import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandlerPlatform import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandlerPlatform
import org.koin.dsl.module import org.koin.dsl.module
@@ -21,4 +23,5 @@ actual val platformModule = module {
name = "app.db" 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 { class PermissionHandlerPlatform: PermissionHandler {
@Composable @Composable
override fun HandlePermissionRequest( override fun HandlePermissionRequest(
onPermissionResult: (Boolean, Boolean) -> Unit, onPermissionResult: (Boolean) -> Unit,
content: @Composable (requestPermissions: () -> Unit) -> Unit content: @Composable (requestPermissions: () -> Unit) -> Unit
) { ) {
val bluetoothManager: BluetoothManager = koinInject() val bluetoothManager: BluetoothManager = koinInject()
@@ -24,12 +24,12 @@ class PermissionHandlerPlatform: PermissionHandler {
// will update asynchronously via the CBCentralManagerDelegate // will update asynchronously via the CBCentralManagerDelegate
val granted = bluetoothManager.hasBluetoothPermission.value val granted = bluetoothManager.hasBluetoothPermission.value
val denied = !granted onPermissionResult(granted)
onPermissionResult(granted, denied)
} }
} }
private val _isLocationEnabled = MutableStateFlow(true) private val _isLocationEnabled = MutableStateFlow(true)
override val isLocationEnabled: StateFlow<Boolean> = _isLocationEnabled.asStateFlow() override val isLocationEnabled: StateFlow<Boolean> = _isLocationEnabled.asStateFlow()
override fun enableLocation() { /* no-op */ } override fun enableLocation() { /* no-op */ }
override fun checkLocationState() { /* no-op */ } override fun checkLocationState() { /* no-op */ }
override val hasBleFeature: Boolean = true
} }