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 androidx.annotation.RequiresApi
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 com.example.smart_watch_android.MainActivity
import com.example.smart_watch_android.ViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -28,63 +27,67 @@ import java.time.format.DateTimeFormatter
import java.util.UUID
import kotlin.time.Duration.Companion.milliseconds
class BleManager(private val context: Context) {
val deviceAddress = "10:BD:A3:8F:01:0E"
class BleManager(private val context: Context, val viewModel: ViewModel) {
//var deviceAddress = "10:BD:A3:8F:01:0E"
val devices = mutableListOf<Devices>()
val currentDate: LocalDate? = LocalDate.now()
val deviceUUIDs = DeviceUUIDs
private val deviceUUIDs = DeviceUUIDs
private var bluetoothGatt: BluetoothGatt? = null
private var bluetoothManager: BluetoothManager? = 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)
private set
gatt.close()
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)
fun init() {
checkAndRequestBluetoothPermissions()
bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
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)
fun checkCurrentConnectionState(deviceAddress: String) {
if (bluetoothManager == null) return
try {
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}")
fun getDeviceList(): List<Devices>{
bluetoothAdapter?.bondedDevices?.forEach { device ->
devices.add(Devices(deviceName = device.name, deviceMac = device.address))
}
return devices
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
@RequiresApi(Build.VERSION_CODES.S)
fun checkAndRequestBluetoothPermissions() {
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")
if (bluetoothAdapter == null) {
Log.d("ESP32_BLE", "error: Bluetooth is not supported!")
@@ -117,34 +120,6 @@ class BleManager(private val context: Context) {
try {
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) {
bluetoothGatt = device.connectGatt(context, false, gattCallback)
} 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
import android.Manifest
import android.content.Intent
import android.os.Build
import android.os.Bundle
@@ -8,37 +9,41 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.example.smart_watch_android.BLE.BleManager
import com.example.smart_watch_android.Screens.AppNavigation
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
lateinit var bleManager: BleManager
private val viewModel: ViewModel by viewModels()
@RequiresApi(Build.VERSION_CODES.S)
@androidx.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.initBleManager(this)
viewModel.bleManager.checkAndRequestBluetoothPermissions()
bleManager = BleManager(this, viewModel)
bleManager.init()
enableEdgeToEdge()
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
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
import android.Manifest
import android.R
import androidx.annotation.RequiresPermission
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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.composable
import androidx.navigation.compose.rememberNavController
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)
@Composable
fun AppNavigation(bleManager: BleManager) {
fun AppNavigation(bleManager: BleManager, viewModel: ViewModel) {
val navController = rememberNavController()
val isConnected by viewModel.isDeviceConnected.collectAsState()
LaunchedEffect(bleManager.isConnected) {
if (bleManager.isConnected) {
LaunchedEffect(viewModel.isConnected) {
if (isConnected) {
navController.navigate("control_panel") {
popUpTo("connect") { inclusive = true }
}
@@ -29,7 +29,7 @@ fun AppNavigation(bleManager: BleManager) {
}
}
}
val startDestination = if (bleManager.isConnected) {
val startDestination = if (isConnected) {
"control_panel"
} else {
"connect"
@@ -42,7 +42,8 @@ fun AppNavigation(bleManager: BleManager) {
// Connect Screen
composable("connect") {
ConnectScreen(
bleManager = bleManager
bleManager = bleManager,
viewModel = viewModel
)
}
@@ -1,5 +1,8 @@
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.Box
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.sp
import com.example.smart_watch_android.BLE.BleManager
import com.example.smart_watch_android.ViewModel
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
@Composable
fun ConnectScreen(bleManager: BleManager) {
fun ConnectScreen(bleManager: BleManager, viewModel: ViewModel) {
var expanded by remember { mutableStateOf(false) }
var selectedDevice by remember { mutableStateOf("device") }
val items = listOf("Option 1", "Option 2", "Option 3")
val devices = bleManager.getDeviceList()
Column(
modifier = Modifier
.fillMaxSize()
@@ -53,11 +58,13 @@ fun ConnectScreen(bleManager: BleManager) {
expanded = expanded,
onDismissRequest = { expanded = false }
) {
items.forEach { item ->
devices.forEach { item ->
DropdownMenuItem(
text = { Text(item) },
text = { Text(item.deviceName) },
onClick = {
selectedDevice = item
selectedDevice = item.deviceName
bleManager.deviceAddress = item.deviceMac
Log.d("ConnectScreen", item.deviceMac)
expanded = false
}
)
@@ -67,8 +74,8 @@ fun ConnectScreen(bleManager: BleManager) {
Spacer(modifier = Modifier.height(64.dp))
Button(
onClick = { bleManager.connectToEsp32(deviceAddress = "10:BD:A3:8F:01:0E") },
modifier = Modifier.size(400.dp),
onClick = {bleManager.connectToEsp32() },
shape = CircleShape,
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
}
}