diff --git a/app/.idea/.gitignore b/app/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/app/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/app/.idea/AndroidProjectSystem.xml b/app/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/app/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/.idea/caches/deviceStreaming.xml b/app/.idea/caches/deviceStreaming.xml new file mode 100644 index 0000000..8cc741b --- /dev/null +++ b/app/.idea/caches/deviceStreaming.xml @@ -0,0 +1,2410 @@ + + + + + + \ No newline at end of file diff --git a/app/.idea/gradle.xml b/app/.idea/gradle.xml new file mode 100644 index 0000000..b838237 --- /dev/null +++ b/app/.idea/gradle.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.idea/migrations.xml b/app/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/app/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/.idea/misc.xml b/app/.idea/misc.xml new file mode 100644 index 0000000..3aec57f --- /dev/null +++ b/app/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/.idea/runConfigurations.xml b/app/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/app/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/.idea/vcs.xml b/app/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/app/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9bda6b3..3a6e55c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,5 +54,6 @@ dependencies { debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.tooling) + implementation("androidx.activity:activity-ktx:1.13.0") implementation("androidx.navigation:navigation-compose:2.10.2") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4ca46b6..1624f43 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> - + @@ -19,7 +19,6 @@ android:theme="@style/Theme.SmartWatchAndroid"> diff --git a/app/src/main/java/com/example/smart_watch_android/BLE/BleManager.kt b/app/src/main/java/com/example/smart_watch_android/BLE/BleManager.kt index 7529169..8387f48 100644 --- a/app/src/main/java/com/example/smart_watch_android/BLE/BleManager.kt +++ b/app/src/main/java/com/example/smart_watch_android/BLE/BleManager.kt @@ -9,16 +9,22 @@ import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.util.Log +import android.widget.Toast import androidx.annotation.RequiresApi import androidx.annotation.RequiresPermission -import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.LocalContext import androidx.core.app.ActivityCompat +import com.example.smart_watch_android.MainViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.time.LocalDate import java.time.LocalTime @@ -26,18 +32,15 @@ import java.time.format.DateTimeFormatter import java.util.UUID import kotlin.time.Duration.Companion.milliseconds -class BleManager(private val context: Context) { - - //var deviceAddress = "10:BD:A3:8F:01:0E" +object BLEManager{ var devices = mutableListOf() - var isConnected = mutableStateOf(false) - + private val _isConnected = MutableStateFlow(false) + val isConnected: StateFlow = _isConnected.asStateFlow() val currentDate: LocalDate? = LocalDate.now() private val deviceUUIDs = DeviceUUIDs private var bluetoothGatt: BluetoothGatt? = null - private var bluetoothManager: BluetoothManager? = null - private var bluetoothAdapter: BluetoothAdapter? = null - var deviceAddress: String = "" + private lateinit var bluetoothManager: BluetoothManager + private lateinit var bluetoothAdapter: BluetoothAdapter val gattCallback = object : BluetoothGattCallback() { @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { @@ -46,7 +49,7 @@ class BleManager(private val context: Context) { Log.d("ESP32_BLE", "Connected! Start discoverServices()...") gatt.discoverServices() } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { - isConnected.value = false + updateConnectionStatus(false) gatt.close() bluetoothGatt = null @@ -54,14 +57,14 @@ class BleManager(private val context: Context) { } } else { Log.d("ESP32_BLE", "error in onConnectionStateChange! Status-Code: $status") - isConnected.value = false + updateConnectionStatus(false) } } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { Log.d("ESP32_BLE", "connected successfully.") - isConnected.value = true + updateConnectionStatus(true) } } } @@ -69,28 +72,31 @@ class BleManager(private val context: Context) { val deviceName: String, val deviceMac: String ) - @RequiresApi(Build.VERSION_CODES.S) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - fun init() { - checkAndRequestBluetoothPermissions() + fun init(context: Context, activity: Activity) { + checkAndRequestBluetoothPermissions( + activity = activity + ) bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - bluetoothAdapter = bluetoothManager?.adapter + bluetoothAdapter = bluetoothManager.adapter devices = getDeviceList() as MutableList } - + fun updateConnectionStatus(status: Boolean) { + _isConnected.value = status + } @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun getDeviceList(): List{ - bluetoothAdapter?.bondedDevices?.forEach { device -> + devices.clear() + bluetoothAdapter.bondedDevices?.forEach { device -> devices.add(Devices(deviceName = device.name, deviceMac = device.address)) } Log.d("devices","$devices") return devices } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @RequiresApi(Build.VERSION_CODES.S) - fun checkAndRequestBluetoothPermissions() { + fun checkAndRequestBluetoothPermissions(activity: Activity) { val permissions = arrayOf( Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN, @@ -98,27 +104,20 @@ class BleManager(private val context: Context) { ) val missingPermissions = permissions.filter { - ActivityCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED + ActivityCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED } if (missingPermissions.isNotEmpty()) { - val activity = context as? Activity - if (activity != null) { - ActivityCompat.requestPermissions(activity, missingPermissions.toTypedArray(), 101) - } else { - Log.d("ESP32_BLE", "Cannot request permissions: Context is not an Activity!") - } + // Berechtigungen anfordern + ActivityCompat.requestPermissions(activity, missingPermissions.toTypedArray(), 101) + } else { + Log.d("ESP32_BLE", "Alle Bluetooth-Berechtigungen sind bereits erteilt!") } } - - fun connectToEsp32() { + fun connectToEsp32(context: Context, deviceAddress: String) { Log.d("ESP32_BLE", "connectToEsp32 started with address: $deviceAddress") - if (bluetoothAdapter == null) { - Log.d("ESP32_BLE", "error: Bluetooth is not supported!") - return - } try { - val device = bluetoothAdapter!!.getRemoteDevice(deviceAddress) + val device = bluetoothAdapter.getRemoteDevice(deviceAddress) if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) { bluetoothGatt = device.connectGatt(context, false, gattCallback) @@ -131,7 +130,7 @@ class BleManager(private val context: Context) { } } @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - fun disconnectEsp32() { + fun disconnectEsp32(context: Context) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) { bluetoothGatt?.disconnect() Log.d("ESP32_BLE","disconnectEsp32") @@ -140,8 +139,7 @@ class BleManager(private val context: Context) { Log.d("BLE_DEBUG", "Permission BLUETOOTH_CONNECT error!") } } - - fun writeToEsp32(dataString: String, SERVICE_UUID: UUID?, CHARACTERISTIC_UUID: UUID?) { + fun writeToEsp32(context: Context, dataString: String, SERVICE_UUID: UUID?, CHARACTERISTIC_UUID: UUID?) { val gatt = bluetoothGatt if (gatt == null) { Log.d("ESP32_BLE", "error: no GATT-Objekt existing (not connected).") @@ -170,7 +168,7 @@ class BleManager(private val context: Context) { Log.d("ESP32_BLE", "started writing: $success") } } - fun syncTime() { + fun syncTime(context: Context) { val currentTime = LocalTime.now() val formatterTime = DateTimeFormatter.ofPattern("HH mm ss") val formattedTime = currentTime.format(formatterTime) @@ -181,11 +179,19 @@ class BleManager(private val context: Context) { Log.d("Time/Date","$formattedDate $formattedTime") CoroutineScope(Dispatchers.IO).launch { - writeToEsp32(formattedTime, deviceUUIDs.SERVICE_UUID, deviceUUIDs.CHAR_TIME_UUID) + writeToEsp32( + context = context, + dataString = formattedTime, + SERVICE_UUID = deviceUUIDs.SERVICE_UUID, + CHARACTERISTIC_UUID = deviceUUIDs.CHAR_TIME_UUID) delay(150.milliseconds) - writeToEsp32("$formattedDate", deviceUUIDs.SERVICE_UUID, deviceUUIDs.CHAR_DATE_UUID) + writeToEsp32( + context = context, + dataString ="$formattedDate", + SERVICE_UUID = deviceUUIDs.SERVICE_UUID, + CHARACTERISTIC_UUID = deviceUUIDs.CHAR_DATE_UUID) } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/smart_watch_android/MainActivity.kt b/app/src/main/java/com/example/smart_watch_android/MainActivity.kt index 88d1534..7d46369 100644 --- a/app/src/main/java/com/example/smart_watch_android/MainActivity.kt +++ b/app/src/main/java/com/example/smart_watch_android/MainActivity.kt @@ -1,7 +1,6 @@ package com.example.smart_watch_android import android.Manifest -import android.content.Intent import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity @@ -13,20 +12,17 @@ import androidx.annotation.RequiresPermission import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.example.smart_watch_android.BLE.BleManager +import com.example.smart_watch_android.BLE.BLEManager import com.example.smart_watch_android.Screens.AppNavigation class MainActivity : ComponentActivity() { - lateinit var bleManager: BleManager - + private val viewModel: MainViewModel by viewModels() @RequiresApi(Build.VERSION_CODES.S) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) override fun onCreate(savedInstanceState: Bundle?,) { super.onCreate(savedInstanceState) - bleManager = BleManager(this) - bleManager.init() + BLEManager.init(context = applicationContext, activity = this) enableEdgeToEdge() setContent { MaterialTheme { @@ -34,16 +30,9 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - AppNavigation(bleManager = bleManager) + AppNavigation(viewModel) } } } } - - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - override fun onDestroy() { - super.onDestroy() - bleManager.disconnectEsp32() - - } } diff --git a/app/src/main/java/com/example/smart_watch_android/MainViewModel.kt b/app/src/main/java/com/example/smart_watch_android/MainViewModel.kt new file mode 100644 index 0000000..f8a50d6 --- /dev/null +++ b/app/src/main/java/com/example/smart_watch_android/MainViewModel.kt @@ -0,0 +1,17 @@ +package com.example.smart_watch_android + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel + +class MainViewModel : ViewModel() { + var selectedDevice by mutableStateOf("device") + fun updateSelectedDevice(newName: String) { + selectedDevice = newName + } + var selectedDeviceAddress = "" + fun updateSelectedDeviceAddress(address: String) { + selectedDeviceAddress = address + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smart_watch_android/Screens/AppNavigation.kt b/app/src/main/java/com/example/smart_watch_android/Screens/AppNavigation.kt index 42d7701..468da28 100644 --- a/app/src/main/java/com/example/smart_watch_android/Screens/AppNavigation.kt +++ b/app/src/main/java/com/example/smart_watch_android/Screens/AppNavigation.kt @@ -4,17 +4,23 @@ import android.Manifest import androidx.annotation.RequiresPermission import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext 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.BLE.BLEManager +import com.example.smart_watch_android.MainViewModel @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @Composable -fun AppNavigation(bleManager: BleManager) { +fun AppNavigation(viewModel: MainViewModel) { + val context = LocalContext.current + val isConnected by BLEManager.isConnected.collectAsState() val navController = rememberNavController() - LaunchedEffect(bleManager.isConnected) { - if (bleManager.isConnected.value) { + LaunchedEffect(BLEManager.isConnected) { + if (isConnected) { navController.navigate("control_panel") { popUpTo("connect") { inclusive = true } } @@ -24,7 +30,7 @@ fun AppNavigation(bleManager: BleManager) { } } } - val startDestination = if (bleManager.isConnected.value) { + val startDestination = if (isConnected) { "control_panel" } else { "connect" @@ -36,17 +42,15 @@ fun AppNavigation(bleManager: BleManager) { ) { // Connect Screen composable("connect") { - ConnectScreen( - bleManager = bleManager - ) + ConnectScreen(viewModel) } composable("control_panel") { ControlPanelScreen( - bleManager = bleManager, + viewModel, onDisconnectClicked = { - bleManager.disconnectEsp32() - } + BLEManager.disconnectEsp32(context = context) + }, ) } } diff --git a/app/src/main/java/com/example/smart_watch_android/Screens/ConnectScreen.kt b/app/src/main/java/com/example/smart_watch_android/Screens/ConnectScreen.kt index 9830b90..9481a93 100644 --- a/app/src/main/java/com/example/smart_watch_android/Screens/ConnectScreen.kt +++ b/app/src/main/java/com/example/smart_watch_android/Screens/ConnectScreen.kt @@ -1,13 +1,17 @@ package com.example.smart_watch_android.Screens import android.Manifest +import android.content.res.Configuration 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 import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -27,15 +31,29 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext 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.BLE.BLEManager +import com.example.smart_watch_android.MainViewModel @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @Composable -fun ConnectScreen(bleManager: BleManager) { +fun ConnectScreen(viewModel: MainViewModel) { + val context = LocalContext.current var expanded by remember { mutableStateOf(false) } - var selectedDevice by remember { mutableStateOf("device") } + val configuration = LocalConfiguration.current + val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + val fraction = 0.7f + val dynamicModifier = if (isLandscape) { + Modifier + .fillMaxHeight(fraction) + .aspectRatio(1f) + } else { + Modifier + .fillMaxWidth(fraction) + .aspectRatio(1f) + } Column( modifier = Modifier .fillMaxSize() @@ -44,29 +62,34 @@ fun ConnectScreen(bleManager: BleManager) { horizontalAlignment = Alignment.CenterHorizontally ) { + Box( + ){ + Text("if the list is empty ensure you have bluetooth enabled!") + } Box( modifier = Modifier .padding(16.dp) ) { Button( - onClick = { expanded = !expanded }, + onClick = { expanded = !expanded}, shape = RoundedCornerShape(4.dp), - modifier = Modifier.width(400.dp) + modifier = Modifier.fillMaxWidth(fraction), + colors = ButtonDefaults.buttonColors(Color(0xFF5E18BE)) ) { - Text(selectedDevice) + Text(viewModel.selectedDevice) } DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, modifier = Modifier - .width(400.dp) + .fillMaxWidth(0.6f) ) { - bleManager.devices.forEach { item -> + BLEManager.devices.forEach { item -> DropdownMenuItem( text = { Text(item.deviceName) }, onClick = { - selectedDevice = item.deviceName - bleManager.deviceAddress = item.deviceMac + viewModel.updateSelectedDevice(item.deviceName) + viewModel.updateSelectedDeviceAddress(item.deviceMac) Log.d("ConnectScreen", item.deviceMac) expanded = false } @@ -75,13 +98,11 @@ fun ConnectScreen(bleManager: BleManager) { } } - - Spacer(modifier = Modifier.height(64.dp)) Button( - modifier = Modifier.size(400.dp), - onClick = {bleManager.connectToEsp32() }, + modifier = dynamicModifier, + onClick = {BLEManager.connectToEsp32(context, viewModel.selectedDeviceAddress) }, shape = CircleShape, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF5E18BE)) ) { diff --git a/app/src/main/java/com/example/smart_watch_android/Screens/ControlPanelScreen.kt b/app/src/main/java/com/example/smart_watch_android/Screens/ControlPanelScreen.kt index 573108d..db5081d 100644 --- a/app/src/main/java/com/example/smart_watch_android/Screens/ControlPanelScreen.kt +++ b/app/src/main/java/com/example/smart_watch_android/Screens/ControlPanelScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -19,15 +20,18 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import com.example.smart_watch_android.BLE.BleManager +import com.example.smart_watch_android.BLE.BLEManager import com.example.smart_watch_android.BLE.DeviceUUIDs +import com.example.smart_watch_android.MainViewModel @Composable fun ControlPanelScreen( - bleManager: BleManager, + viewModel: MainViewModel, onDisconnectClicked: () -> Unit ) { + val context = LocalContext.current var textInput by remember { mutableStateOf("") } Column( @@ -50,9 +54,12 @@ fun ControlPanelScreen( Button( onClick = { if (textInput.isNotEmpty()) { - bleManager.writeToEsp32( - textInput, DeviceUUIDs.SERVICE_UUID, DeviceUUIDs.CHAR_MESSAGE_UUID - ) + BLEManager.writeToEsp32( + context = context, + dataString = textInput, + SERVICE_UUID = DeviceUUIDs.SERVICE_UUID, + CHARACTERISTIC_UUID = DeviceUUIDs.CHAR_MESSAGE_UUID, + ) textInput = "" } }, @@ -67,7 +74,9 @@ fun ControlPanelScreen( Button( onClick = { - bleManager.syncTime() + BLEManager.syncTime( + context + ) }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF50128F)), modifier = Modifier.fillMaxWidth()