Handle request runtime permission
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- Android 6.0–11 -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<!-- 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_CONNECT" tools:targetApi="31"/>
|
||||
|
||||
<!-- Allow access http: android:usesCleartextTraffic="true" -->
|
||||
<application
|
||||
android:name=".MyApplication"
|
||||
|
||||
+3
@@ -7,12 +7,15 @@ 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.BluetoothManager
|
||||
import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler
|
||||
import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandlerPlatform
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
actual val platformModule = module {
|
||||
single<AppStorage> { AppStoragePlatform(androidContext()) }
|
||||
single<BluetoothManager> { AndroidBluetoothManager(androidContext()) }
|
||||
single<PermissionHandler> { PermissionHandlerPlatform(androidContext()) }
|
||||
single<SqlDriver> {
|
||||
AndroidSqliteDriver(
|
||||
schema = AppDatabase.Schema,
|
||||
|
||||
+92
-14
@@ -1,36 +1,114 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.utils
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.app.ActivityCompat
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
@Composable
|
||||
actual fun HandlePermissionRequest(
|
||||
onPermissionResult: (Boolean) -> Unit
|
||||
) {
|
||||
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
class PermissionHandlerPlatform(private val context: Context): PermissionHandler {
|
||||
@SuppressLint("ContextCastToActivity")
|
||||
@Composable
|
||||
override fun HandlePermissionRequest (
|
||||
onPermissionResult: (Boolean, Boolean) -> Unit,
|
||||
content: @Composable (requestPermissions: () -> Unit) -> Unit
|
||||
) {
|
||||
val permissions = if (isPreAndroid12()) {
|
||||
/** For android 11- */
|
||||
arrayOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
)
|
||||
} else {
|
||||
/** For android 12+ */
|
||||
arrayOf(
|
||||
Manifest.permission.BLUETOOTH_SCAN,
|
||||
Manifest.permission.BLUETOOTH_CONNECT
|
||||
)
|
||||
} else {
|
||||
arrayOf(
|
||||
Manifest.permission.BLUETOOTH,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
)
|
||||
}
|
||||
|
||||
val activity: Activity? = LocalContext.current as? Activity
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { result ->
|
||||
val allGranted = result.values.all { it }
|
||||
onPermissionResult(allGranted)
|
||||
}
|
||||
val permanentlyDenied = if (isPreAndroid12()) {
|
||||
/** For android 6.0 - 11 */
|
||||
val deniedLocation = result[Manifest.permission.ACCESS_FINE_LOCATION] == false
|
||||
deniedLocation && activity?.let {
|
||||
!ActivityCompat.shouldShowRequestPermissionRationale(it,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
} ?: false
|
||||
} else {
|
||||
/** For android 12+ */
|
||||
val deniedScan = result[Manifest.permission.BLUETOOTH_SCAN] == false
|
||||
val deniedConnect = result[Manifest.permission.BLUETOOTH_CONNECT] == false
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val permDeniedScan = deniedScan && activity?.let {
|
||||
!ActivityCompat.shouldShowRequestPermissionRationale(it,
|
||||
Manifest.permission.BLUETOOTH_SCAN)
|
||||
} ?: false
|
||||
|
||||
val permDeniedConnect = deniedConnect && activity?.let {
|
||||
!ActivityCompat.shouldShowRequestPermissionRationale(it,
|
||||
Manifest.permission.BLUETOOTH_CONNECT)
|
||||
} ?: false
|
||||
permDeniedScan || permDeniedConnect
|
||||
}
|
||||
onPermissionResult(allGranted, permanentlyDenied)
|
||||
}
|
||||
content {
|
||||
launcher.launch(permissions)
|
||||
}
|
||||
}
|
||||
private fun isPreAndroid12(): Boolean {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.S
|
||||
}
|
||||
private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
private val _isLocationEnabled = MutableStateFlow(true)
|
||||
override val isLocationEnabled: StateFlow<Boolean> = _isLocationEnabled.asStateFlow()
|
||||
init {
|
||||
if(isPreAndroid12()) {
|
||||
val providerChangedReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == LocationManager.PROVIDERS_CHANGED_ACTION) {
|
||||
checkLocationState()
|
||||
}
|
||||
}
|
||||
}
|
||||
checkLocationState()
|
||||
context.registerReceiver(providerChangedReceiver,
|
||||
IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)
|
||||
)
|
||||
}
|
||||
}
|
||||
override fun checkLocationState() {
|
||||
if(isPreAndroid12()) {
|
||||
try {
|
||||
_isLocationEnabled.value =
|
||||
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
|
||||
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||
} catch (e: SecurityException) {
|
||||
_isLocationEnabled.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun enableLocation() {
|
||||
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -44,4 +44,32 @@
|
||||
<string name="action_retry">Retry</string>
|
||||
<string name="device_connecting">Connecting…</string>
|
||||
<string name="device_explanation">It should just take a moment…</string>
|
||||
|
||||
<string name="action_grant_permission">Grant permission</string>
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="action_enable">Enable</string>
|
||||
<string name="action_try_again">Try again</string>
|
||||
<string name="action_location_settings">Location settings</string>
|
||||
<string name="action_go_up">Back</string>
|
||||
|
||||
<string name="bluetooth_disabled_title">BLUETOOTH DISABLED</string>
|
||||
<string name="bluetooth_disabled_info">The Bluetooth adapter is turned off.\nClick the button below to enable it.</string>
|
||||
|
||||
<string name="location_permission_title">LOCATION PERMISSION REQUIRED</string>
|
||||
<string name="location_permission_info">From Android 6.0 Marshmallow until Android 11 the application requires Location permission in order to scan for Bluetooth Low Energy devices. This is because Bluetooth LE beacons, for example iBeacons or Eddystone, may be used to
|
||||
determine the phone's and user's location.\n
|
||||
The app will not use this information in any way.\n
|
||||
On Android 12 or newer a new Nearby permission can be requested instead, without the need for location.</string>
|
||||
|
||||
<string name="bluetooth_permission_title">BLUETOOTH PERMISSIONS REQUIRED</string>
|
||||
<string name="bluetooth_permission_info">The app needs to scan and connect to Bluetooth LE devices.</string>
|
||||
|
||||
<string name="bluetooth_not_available_title">BLUETOOTH NOT AVAILABLE</string>
|
||||
<string name="bluetooth_not_available_info">Bluetooth is not available on your phone.\nWe cannot provide you the feature.</string>
|
||||
<string name="location_required_title">Location required</string>
|
||||
<string name="bluetooth_required_title">Bluetooth required</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
|
||||
Click the button below to enable Location.</string>
|
||||
</resources>
|
||||
|
||||
@@ -6,6 +6,7 @@ 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.theme.TorqueVaultTheme
|
||||
import io.github.aakira.napier.Napier
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
|
||||
+6
-5
@@ -1,5 +1,6 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
@@ -101,8 +102,8 @@ fun WarningView(
|
||||
) {
|
||||
WarningView(
|
||||
painterResource = painterResource,
|
||||
title = { Text(text = title) },
|
||||
hint = { Hint(text = hint, textAlign = hintTextAlign) },
|
||||
title = { Text(text = title, color = MaterialTheme.colorScheme.onBackground) },
|
||||
hint = { Hint(text = hint, textAlign = hintTextAlign, color = MaterialTheme.colorScheme.onBackground) },
|
||||
modifier = modifier,
|
||||
content = content,
|
||||
)
|
||||
@@ -128,8 +129,8 @@ fun WarningView(
|
||||
) {
|
||||
WarningView(
|
||||
painterResource = painterResource,
|
||||
title = { Text(text = title) },
|
||||
hint = { Hint(text = hint, textAlign = hintTextAlign) },
|
||||
title = { Text(text = title, color = MaterialTheme.colorScheme.onBackground) },
|
||||
hint = { Hint(text = hint, textAlign = hintTextAlign, color = MaterialTheme.colorScheme.onBackground) },
|
||||
modifier = modifier,
|
||||
content = content,
|
||||
)
|
||||
@@ -179,7 +180,7 @@ fun WarningView(
|
||||
content: @Composable ColumnScope.() -> Unit = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
modifier = modifier.background(MaterialTheme.colorScheme.background),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerScreen
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerViewModel
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingDestination
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingScreen
|
||||
import io.github.aakira.napier.Napier
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
+42
-63
@@ -1,20 +1,10 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BluetoothDisabled
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -22,55 +12,62 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.HandlePermissionRequest
|
||||
import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@Composable
|
||||
fun BluetoothRequirementWrapper(
|
||||
content: @Composable () -> Unit
|
||||
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) {
|
||||
HandlePermissionRequest { _ ->
|
||||
permissionHandler.HandlePermissionRequest(
|
||||
{ allGranted, denied ->
|
||||
showPermissionRequest = false
|
||||
bluetoothManager.checkBluetoothPermission()
|
||||
hasPermanentlyDenied = denied
|
||||
}
|
||||
) {
|
||||
requestPermissions ->
|
||||
LaunchedEffect(Unit) {
|
||||
requestPermissions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LifecycleResumeEffect(Unit) {
|
||||
bluetoothManager.checkBluetoothState()
|
||||
bluetoothManager.checkBluetoothPermission()
|
||||
permissionHandler.checkLocationState()
|
||||
onPauseOrDispose { }
|
||||
}
|
||||
|
||||
LaunchedEffect(isEnabled, hasPermission) {
|
||||
if (!isEnabled && hasPermission) {
|
||||
bluetoothManager.enableBluetooth()
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnabled && hasPermission) {
|
||||
/* MainScreen */
|
||||
content()
|
||||
} else {
|
||||
/* Overlay content */
|
||||
if (!isEnabled || !hasPermission || !isLocationEnabled) {
|
||||
BluetoothRequestScreen(
|
||||
isEnabled = isEnabled,
|
||||
hasPermission = hasPermission,
|
||||
hasPermanentlyDenied = hasPermanentlyDenied,
|
||||
isLocationEnabled = isLocationEnabled,
|
||||
onEnableBluetooth = { bluetoothManager.enableBluetooth() },
|
||||
onRequestPermission = {
|
||||
bluetoothManager.enableBluetooth()
|
||||
showPermissionRequest = true
|
||||
},
|
||||
onEnableLocation = { permissionHandler.enableLocation() },
|
||||
onRequestPermission = { showPermissionRequest = true },
|
||||
onOpenSettings = { bluetoothManager.openSettings() }
|
||||
)
|
||||
}
|
||||
@@ -80,46 +77,28 @@ fun BluetoothRequirementWrapper(
|
||||
fun BluetoothRequestScreen(
|
||||
isEnabled: Boolean,
|
||||
hasPermission: Boolean,
|
||||
hasPermanentlyDenied: Boolean,
|
||||
isLocationEnabled: Boolean,
|
||||
onEnableBluetooth: () -> Unit,
|
||||
onEnableLocation: () -> Unit,
|
||||
onRequestPermission: () -> Unit,
|
||||
onOpenSettings: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (!hasPermission) Icons.Default.Security else Icons.Default.BluetoothDisabled,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
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
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = if (!hasPermission) "Bluetooth Permission Required" else "Bluetooth is Disabled",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = if (!hasPermission)
|
||||
"This app needs Bluetooth permissions to connect to your devices. Please grant permission."
|
||||
else "Please enable Bluetooth to use this app's features.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
if (!hasPermission) {
|
||||
Button(onClick = onRequestPermission) {
|
||||
Text("Request Permission")
|
||||
}
|
||||
} else if (!isEnabled) {
|
||||
Button(onClick = onEnableBluetooth) {
|
||||
Text("Enable Bluetooth")
|
||||
}
|
||||
!isLocationEnabled -> LocationDisabledView(onEnable = onEnableLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.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
|
||||
import androidx.compose.material.icons.rounded.BluetoothDisabled
|
||||
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
|
||||
import torquevaultkmp.composeapp.generated.resources.action_enable
|
||||
import torquevaultkmp.composeapp.generated.resources.bluetooth_disabled_info
|
||||
import torquevaultkmp.composeapp.generated.resources.bluetooth_disabled_title
|
||||
|
||||
@Composable
|
||||
internal fun BluetoothDisabledView(
|
||||
onEnable: () -> Unit,
|
||||
) {
|
||||
WarningView(
|
||||
imageVector = Icons.Rounded.BluetoothDisabled,
|
||||
title = stringResource(Res.string.bluetooth_disabled_title),
|
||||
hint = stringResource(Res.string.bluetooth_disabled_info),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Button(onClick = onEnable) {
|
||||
Text(text = stringResource(Res.string.action_enable))
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.views
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.baseline_bluetooth_disabled_24
|
||||
import torquevaultkmp.composeapp.generated.resources.bluetooth_not_available_info
|
||||
import torquevaultkmp.composeapp.generated.resources.bluetooth_not_available_title
|
||||
|
||||
@Composable
|
||||
internal fun BluetoothNotAvailableView() {
|
||||
WarningView(
|
||||
painterResource = painterResource(Res.drawable.baseline_bluetooth_disabled_24),
|
||||
title = stringResource(Res.string.bluetooth_not_available_title),
|
||||
hint = stringResource(Res.string.bluetooth_not_available_info),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
)
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.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
|
||||
import torquevaultkmp.composeapp.generated.resources.action_settings
|
||||
import torquevaultkmp.composeapp.generated.resources.baseline_bluetooth_disabled_24
|
||||
import torquevaultkmp.composeapp.generated.resources.bluetooth_permission_info
|
||||
import torquevaultkmp.composeapp.generated.resources.bluetooth_permission_title
|
||||
|
||||
@Composable
|
||||
internal fun BluetoothPermissionRequiredView(
|
||||
hasPermission: Boolean,
|
||||
hasPermanentlyDenied: Boolean,
|
||||
onRequestPermission: () -> Unit,
|
||||
onOpenSetting: () -> Unit
|
||||
) {
|
||||
WarningView(
|
||||
imageVector = Icons.Rounded.BluetoothDisabled,
|
||||
title = stringResource(Res.string.bluetooth_permission_title),
|
||||
hint = stringResource(Res.string.bluetooth_permission_info),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
when {
|
||||
!hasPermission -> Button(
|
||||
onClick = { onRequestPermission() }
|
||||
) {
|
||||
Text(text = stringResource(Res.string.action_grant_permission))
|
||||
}
|
||||
!hasPermanentlyDenied -> Button(
|
||||
onClick = { onOpenSetting() }
|
||||
) {
|
||||
Text(text = stringResource(Res.string.action_settings))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.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.LocationOff
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
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.action_enable
|
||||
import torquevaultkmp.composeapp.generated.resources.location_disabled_info
|
||||
import torquevaultkmp.composeapp.generated.resources.location_disabled_title
|
||||
|
||||
@Composable
|
||||
internal fun LocationDisabledView(
|
||||
onEnable: () -> Unit,
|
||||
) {
|
||||
WarningView(
|
||||
imageVector = Icons.Rounded.LocationOff,
|
||||
title = stringResource(Res.string.location_disabled_title),
|
||||
hint = stringResource(Res.string.location_disabled_info),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Button(onClick = onEnable) {
|
||||
Text(text = stringResource(Res.string.action_enable))
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.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
|
||||
import torquevaultkmp.composeapp.generated.resources.action_grant_permission
|
||||
import torquevaultkmp.composeapp.generated.resources.action_settings
|
||||
import torquevaultkmp.composeapp.generated.resources.location_permission_info
|
||||
import torquevaultkmp.composeapp.generated.resources.location_permission_title
|
||||
|
||||
@Composable
|
||||
internal fun LocationPermissionRequiredView(
|
||||
hasPermission: Boolean,
|
||||
hasPermanentlyDenied: Boolean,
|
||||
onRequestPermission: () -> Unit,
|
||||
onOpenSetting: () -> Unit
|
||||
) {
|
||||
WarningView(
|
||||
imageVector = Icons.Rounded.LocationOff,
|
||||
title = stringResource(Res.string.location_permission_title),
|
||||
hint = stringResource(Res.string.location_permission_info),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
when {
|
||||
!hasPermission && !hasPermanentlyDenied -> {
|
||||
Button(onClick = onRequestPermission) {
|
||||
Text(stringResource(Res.string.action_grant_permission))
|
||||
}
|
||||
}
|
||||
!hasPermission && hasPermanentlyDenied -> {
|
||||
Button(onClick = onOpenSetting) {
|
||||
Text(stringResource(Res.string.action_settings))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-4
@@ -1,8 +1,15 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Composable
|
||||
expect fun HandlePermissionRequest(
|
||||
onPermissionResult: (Boolean) -> Unit
|
||||
)
|
||||
interface PermissionHandler {
|
||||
@Composable
|
||||
fun HandlePermissionRequest(
|
||||
onPermissionResult: (Boolean, Boolean) -> Unit,
|
||||
content: @Composable (requestPermissions: () -> Unit) -> Unit
|
||||
)
|
||||
val isLocationEnabled: StateFlow<Boolean>
|
||||
fun enableLocation()
|
||||
fun checkLocationState()
|
||||
}
|
||||
|
||||
+3
@@ -7,11 +7,14 @@ 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.IosBluetoothManager
|
||||
import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler
|
||||
import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandlerPlatform
|
||||
import org.koin.dsl.module
|
||||
|
||||
actual val platformModule = module {
|
||||
single<AppStorage> { AppStoragePlatform() }
|
||||
single<BluetoothManager> { IosBluetoothManager() }
|
||||
single<PermissionHandler> { PermissionHandlerPlatform() }
|
||||
single<SqlDriver> {
|
||||
NativeSqliteDriver(
|
||||
schema = AppDatabase.Schema,
|
||||
|
||||
+13
-13
@@ -65,19 +65,8 @@ class IosBluetoothManager : BluetoothManager {
|
||||
|
||||
val state = centralManager.state
|
||||
if (state == CBCentralManagerStatePoweredOff) {
|
||||
// Attempt to open Bluetooth settings directly via App-Prefs (best effort)
|
||||
val bluetoothUrl = NSURL.URLWithString("App-Prefs:root=Bluetooth")
|
||||
dispatch_async(dispatch_get_main_queue()) {
|
||||
val app = UIApplication.sharedApplication
|
||||
if (bluetoothUrl != null && app.canOpenURL(bluetoothUrl)) {
|
||||
app.openURL(bluetoothUrl, mapOf<Any?, Any?>(), null)
|
||||
} else {
|
||||
// Fallback: Trigger the system's "Bluetooth is Off" power alert
|
||||
// This alert has a "Settings" button that goes to the right place.
|
||||
centralManager.scanForPeripheralsWithServices(null, null)
|
||||
centralManager.stopScan()
|
||||
}
|
||||
}
|
||||
// Bluetooth is off
|
||||
showBluetoothOffAlert()
|
||||
} else if (auth == CBManagerAuthorizationNotDetermined) {
|
||||
// Accessing the state or scanning will trigger the system prompt
|
||||
centralManager.scanForPeripheralsWithServices(null, null)
|
||||
@@ -85,6 +74,17 @@ class IosBluetoothManager : BluetoothManager {
|
||||
checkBluetoothState()
|
||||
}
|
||||
}
|
||||
|
||||
fun showBluetoothOffAlert() {
|
||||
val alert = UIAlertController.alertControllerWithTitle(
|
||||
"Bluetooth is Off",
|
||||
"Please enable Bluetooth in Settings > Bluetooth to use this feature.",
|
||||
UIAlertControllerStyleAlert
|
||||
)
|
||||
alert.addAction(UIAlertAction.actionWithTitle("Close", UIAlertActionStyleDefault, null))
|
||||
|
||||
UIApplication.sharedApplication.keyWindow?.rootViewController?.presentViewController(alert, true, null)
|
||||
}
|
||||
}
|
||||
|
||||
actual fun createBluetoothManager(): BluetoothManager = IosBluetoothManager()
|
||||
|
||||
+18
-5
@@ -2,12 +2,17 @@ package com.digitoolsolutions.app.torquevaultkmp.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@Composable
|
||||
actual fun HandlePermissionRequest(
|
||||
onPermissionResult: (Boolean) -> Unit
|
||||
) {
|
||||
class PermissionHandlerPlatform: PermissionHandler {
|
||||
@Composable
|
||||
override fun HandlePermissionRequest(
|
||||
onPermissionResult: (Boolean, Boolean) -> Unit,
|
||||
content: @Composable (requestPermissions: () -> Unit) -> Unit
|
||||
) {
|
||||
val bluetoothManager: BluetoothManager = koinInject()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -17,6 +22,14 @@ actual fun HandlePermissionRequest(
|
||||
|
||||
// We call the result immediately, but the StateFlows in BluetoothManager
|
||||
// will update asynchronously via the CBCentralManagerDelegate
|
||||
onPermissionResult(bluetoothManager.hasBluetoothPermission.value)
|
||||
|
||||
val granted = bluetoothManager.hasBluetoothPermission.value
|
||||
val denied = !granted
|
||||
onPermissionResult(granted, denied)
|
||||
}
|
||||
}
|
||||
private val _isLocationEnabled = MutableStateFlow(true)
|
||||
override val isLocationEnabled: StateFlow<Boolean> = _isLocationEnabled.asStateFlow()
|
||||
override fun enableLocation() { /* no-op */ }
|
||||
override fun checkLocationState() { /* no-op */ }
|
||||
}
|
||||
Reference in New Issue
Block a user