This commit is contained in:
lash404 committed 2026-10-21 12:18:25 +02:00
1 parent 471741aeb4
commit f54948b1a2
7 files changed
+102 -169

No files matched your search

@@ -14,10 +14,9 @@ import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import com.example.smart_watch_android.MainActivity
import com.example.smart_watch_android.ViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -28,63 +27,67 @@ import java.time.format.DateTimeFormatter
import java.util.UUID import java.util.UUID
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
class BleManager(private val context: Context) { class BleManager(private val context: Context, val viewModel: ViewModel) {
val deviceAddress = "10:BD:A3:8F:01:0E"
//var deviceAddress = "10:BD:A3:8F:01:0E"
val devices = mutableListOf<Devices>()
val currentDate: LocalDate? = LocalDate.now() val currentDate: LocalDate? = LocalDate.now()
val deviceUUIDs = DeviceUUIDs private val deviceUUIDs = DeviceUUIDs
private var bluetoothGatt: BluetoothGatt? = null private var bluetoothGatt: BluetoothGatt? = null
private var bluetoothManager: BluetoothManager? = null private var bluetoothManager: BluetoothManager? = null
private var bluetoothAdapter: BluetoothAdapter? = null private var bluetoothAdapter: BluetoothAdapter? = null
var deviceAddress: String = ""
val gattCallback = object : BluetoothGattCallback() {
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.d("ESP32_BLE", "Connected! Start discoverServices()...")
gatt.discoverServices()
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
viewModel.setDeviceConection(false)
var isConnected by mutableStateOf(false) gatt.close()
private set bluetoothGatt = null
Log.d("ESP32_BLE", "Disconnected.")
}
} else {
Log.d("ESP32_BLE", "error in onConnectionStateChange! Status-Code: $status")
viewModel.setDeviceConection(false)
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
viewModel.setDeviceConection(true)
Log.d("ESP32_BLE", "connected successfully.")
}
}
}
data class Devices(
val deviceName: String,
val deviceMac: String
)
@RequiresApi(Build.VERSION_CODES.S)
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
fun init() { fun init() {
checkAndRequestBluetoothPermissions()
bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager?.adapter bluetoothAdapter = bluetoothManager?.adapter
}
if (bluetoothAdapter == null) return
try {
val device = bluetoothAdapter?.getRemoteDevice(deviceAddress)
if (device != null) {
val state = bluetoothManager!!.getConnectionState(device, BluetoothProfile.GATT)
if (state == BluetoothProfile.STATE_CONNECTED) {
Log.d("ESP32_BLE", "App restarted: device still connected. rebuild GATT-connection...")
connectToEsp32(deviceAddress)
} else {
isConnected = false
Log.d("ESP32_BLE", "App restarted: not connected.")
}
}
} catch (e: Exception) {
Log.d("ESP32_BLE", "error while checking the status: ${e.message}")
}
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
fun checkCurrentConnectionState(deviceAddress: String) { fun getDeviceList(): List<Devices>{
if (bluetoothManager == null) return bluetoothAdapter?.bondedDevices?.forEach { device ->
devices.add(Devices(deviceName = device.name, deviceMac = device.address))
}
return devices
}
try { @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
val device = bluetoothAdapter?.getRemoteDevice(deviceAddress)
if (device != null) {
val state = bluetoothManager!!.getConnectionState(device, BluetoothProfile.GATT)
if (state == BluetoothProfile.STATE_CONNECTED) {
isConnected = true
Log.d("ESP32_BLE", "App restarted: device still connected!")
} else {
isConnected = false
Log.d("ESP32_BLE", "App restarted: device not connected.")
}
}
} catch (e: Exception) {
Log.d("ESP32_BLE", "error while checking the status: ${e.message}")
}
}
@RequiresApi(Build.VERSION_CODES.S) @RequiresApi(Build.VERSION_CODES.S)
fun checkAndRequestBluetoothPermissions() { fun checkAndRequestBluetoothPermissions() {
val permissions = arrayOf( val permissions = arrayOf(
@@ -107,7 +110,7 @@ class BleManager(private val context: Context) {
} }
} }
fun connectToEsp32(deviceAddress: String) { fun connectToEsp32() {
Log.d("ESP32_BLE", "connectToEsp32 started with address: $deviceAddress") Log.d("ESP32_BLE", "connectToEsp32 started with address: $deviceAddress")
if (bluetoothAdapter == null) { if (bluetoothAdapter == null) {
Log.d("ESP32_BLE", "error: Bluetooth is not supported!") Log.d("ESP32_BLE", "error: Bluetooth is not supported!")
@@ -117,34 +120,6 @@ class BleManager(private val context: Context) {
try { try {
val device = bluetoothAdapter!!.getRemoteDevice(deviceAddress) val device = bluetoothAdapter!!.getRemoteDevice(deviceAddress)
val gattCallback = object : BluetoothGattCallback() {
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.d("ESP32_BLE", "Connected! Start discoverServices()...")
gatt.discoverServices()
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
isConnected = false
gatt.close()
bluetoothGatt = null
Log.d("ESP32_BLE", "Disconnected.")
}
} else {
Log.d("ESP32_BLE", "error in onConnectionStateChange! Status-Code: $status")
isConnected = false
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
isConnected = true
Log.d("ESP32_BLE", "connected successfully.")
}
}
}
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
bluetoothGatt = device.connectGatt(context, false, gattCallback) bluetoothGatt = device.connectGatt(context, false, gattCallback)
} else { } else {
@@ -1,47 +0,0 @@
package com.example.smart_watch_android
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.IBinder
import androidx.core.app.NotificationCompat
class BluetoothForegroundService : Service() {
companion object {
const val CHANNEL_ID = "BluetoothServiceChannel"
const val NOTIFICATION_ID = 1
}
override fun onCreate() {
super.onCreate()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
createNotificationChannel()
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Bluetooth connection active")
.setContentText("the app keeps the connection in the background.")
.setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
.build()
startForeground(NOTIFICATION_ID, notification)
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
private fun createNotificationChannel() {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Bluetooth Service Channel",
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(serviceChannel)
}
}
@@ -1,5 +1,6 @@
package com.example.smart_watch_android package com.example.smart_watch_android
import android.Manifest
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -8,21 +9,24 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.example.smart_watch_android.BLE.BleManager
import com.example.smart_watch_android.Screens.AppNavigation import com.example.smart_watch_android.Screens.AppNavigation
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels() lateinit var bleManager: BleManager
private val viewModel: ViewModel by viewModels()
@RequiresApi(Build.VERSION_CODES.S) @RequiresApi(Build.VERSION_CODES.S)
@androidx.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel.initBleManager(this) bleManager = BleManager(this, viewModel)
viewModel.bleManager.checkAndRequestBluetoothPermissions() bleManager.init()
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
MaterialTheme { MaterialTheme {
@@ -30,15 +34,16 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
AppNavigation(bleManager = viewModel.bleManager) AppNavigation(bleManager = bleManager, viewModel = viewModel)
} }
} }
} }
startBluetoothService()
} }
private fun startBluetoothService() {
val serviceIntent = Intent(this, BluetoothForegroundService::class.java)
startForegroundService(serviceIntent) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
override fun onDestroy() {
super.onDestroy()
bleManager.disconnectEsp32()
} }
} }
@@ -1,24 +0,0 @@
package com.example.smart_watch_android
import android.Manifest
import android.app.Application
import android.content.Context
import androidx.annotation.RequiresPermission
import androidx.lifecycle.AndroidViewModel
import com.example.smart_watch_android.BLE.BleManager
class MainViewModel(
application: Application,
) : AndroidViewModel(application) {
lateinit var bleManager: BleManager
private set
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
fun initBleManager(activityContext: Context) {
if (!::bleManager.isInitialized) {
bleManager = BleManager(context = activityContext)
bleManager.init()
}
}
}
@@ -1,25 +1,25 @@
package com.example.smart_watch_android.Screens package com.example.smart_watch_android.Screens
import android.Manifest import android.Manifest
import android.R
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.NavController import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.example.smart_watch_android.BLE.BleManager import com.example.smart_watch_android.BLE.BleManager
import com.example.smart_watch_android.MainViewModel import com.example.smart_watch_android.ViewModel
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
@Composable @Composable
fun AppNavigation(bleManager: BleManager) { fun AppNavigation(bleManager: BleManager, viewModel: ViewModel) {
val navController = rememberNavController() val navController = rememberNavController()
val isConnected by viewModel.isDeviceConnected.collectAsState()
LaunchedEffect(bleManager.isConnected) { LaunchedEffect(viewModel.isConnected) {
if (bleManager.isConnected) { if (isConnected) {
navController.navigate("control_panel") { navController.navigate("control_panel") {
popUpTo("connect") { inclusive = true } popUpTo("connect") { inclusive = true }
} }
@@ -29,7 +29,7 @@ fun AppNavigation(bleManager: BleManager) {
} }
} }
} }
val startDestination = if (bleManager.isConnected) { val startDestination = if (isConnected) {
"control_panel" "control_panel"
} else { } else {
"connect" "connect"
@@ -42,7 +42,8 @@ fun AppNavigation(bleManager: BleManager) {
// Connect Screen // Connect Screen
composable("connect") { composable("connect") {
ConnectScreen( ConnectScreen(
bleManager = bleManager bleManager = bleManager,
viewModel = viewModel
) )
} }
@@ -1,5 +1,8 @@
package com.example.smart_watch_android.Screens package com.example.smart_watch_android.Screens
import android.Manifest
import android.util.Log
import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -28,12 +31,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.example.smart_watch_android.BLE.BleManager import com.example.smart_watch_android.BLE.BleManager
import com.example.smart_watch_android.ViewModel
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
@Composable @Composable
fun ConnectScreen(bleManager: BleManager) { fun ConnectScreen(bleManager: BleManager, viewModel: ViewModel) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
var selectedDevice by remember { mutableStateOf("device") } var selectedDevice by remember { mutableStateOf("device") }
val items = listOf("Option 1", "Option 2", "Option 3") val devices = bleManager.getDeviceList()
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -53,11 +58,13 @@ fun ConnectScreen(bleManager: BleManager) {
expanded = expanded, expanded = expanded,
onDismissRequest = { expanded = false } onDismissRequest = { expanded = false }
) { ) {
items.forEach { item -> devices.forEach { item ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(item) }, text = { Text(item.deviceName) },
onClick = { onClick = {
selectedDevice = item selectedDevice = item.deviceName
bleManager.deviceAddress = item.deviceMac
Log.d("ConnectScreen", item.deviceMac)
expanded = false expanded = false
} }
) )
@@ -67,8 +74,8 @@ fun ConnectScreen(bleManager: BleManager) {
Spacer(modifier = Modifier.height(64.dp)) Spacer(modifier = Modifier.height(64.dp))
Button( Button(
onClick = { bleManager.connectToEsp32(deviceAddress = "10:BD:A3:8F:01:0E") },
modifier = Modifier.size(400.dp), modifier = Modifier.size(400.dp),
onClick = {bleManager.connectToEsp32() },
shape = CircleShape, shape = CircleShape,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF5E18BE)) colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF5E18BE))
) { ) {
@@ -0,0 +1,16 @@
package com.example.smart_watch_android
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class ViewModel : ViewModel() {
var isConnected = MutableStateFlow(false)
val isDeviceConnected: StateFlow<Boolean> = isConnected.asStateFlow()
fun setDeviceConection(status: Boolean) {
isConnected.value = status
}
}