diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 7643783..e28030c 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,5 +1,6 @@ + - \ No newline at end of file + diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 2319ac6..0000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 120708d..2c2f93b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,8 +22,8 @@ android { applicationId "com.cstef.meshlink" minSdk 26 targetSdk 33 - versionCode 29 - versionName "1.2.2" + versionCode 30 + versionName "1.2.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -44,7 +44,7 @@ android { dependencies { implementation 'androidx.core:core-ktx:1.9.0' - implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'androidx.appcompat:appcompat:1.6.0' implementation 'com.google.android.material:material:1.7.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation('com.daveanthonythomas.moshipack:moshipack:1.0.1') { @@ -53,40 +53,47 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-reflect:1.4.10" implementation "androidx.compose.compiler:compiler:$compose_version" - implementation "androidx.compose.runtime:runtime:1.3.2" - implementation 'androidx.compose.material3:material3:1.1.0-alpha03' + implementation "androidx.compose.runtime:runtime:1.3.3" + implementation 'androidx.compose.material3:material3:1.1.0-alpha04' - implementation 'androidx.compose.ui:ui-tooling-preview:1.4.0-alpha03' - debugImplementation 'androidx.compose.ui:ui-tooling:1.4.0-alpha03' + implementation 'androidx.compose.ui:ui-tooling-preview:1.4.0-alpha04' + debugImplementation 'androidx.compose.ui:ui-tooling:1.4.0-alpha04' implementation 'androidx.activity:activity-compose:1.6.1' implementation 'com.google.accompanist:accompanist-systemuicontroller:0.28.0' implementation "com.google.accompanist:accompanist-permissions:0.28.0" // material-icons-extended - implementation 'androidx.compose.material:material-icons-extended:1.4.0-alpha03' + implementation 'androidx.compose.material:material-icons-extended:1.4.0-alpha04' // Navigation implementation 'androidx.navigation:navigation-compose:2.6.0-alpha04' implementation "com.google.accompanist:accompanist-navigation-animation:0.28.0" //AndroidSVG implementation 'com.caverock:androidsvg-aar:1.4' - def room_version = "2.4.3" + // Room + def room_version = "2.5.0" implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" - kapt "androidx.room:room-compiler:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-paging:$room_version" implementation "androidx.room:room-ktx:$room_version" - implementation "androidx.compose.runtime:runtime-livedata:1.3.2" + implementation "androidx.compose.runtime:runtime-livedata:1.3.3" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' + // Database implementation "net.zetetic:android-database-sqlcipher:4.5.2" - implementation "androidx.sqlite:sqlite:2.2.0" + implementation "androidx.sqlite:sqlite:2.3.0" + // QR Code implementation 'com.journeyapps:zxing-android-embedded:4.3.0' + // Easter egg implementation 'nl.dionsegijn:konfetti-compose:2.0.2' + + // Charts + implementation "com.patrykandpatrick.vico:compose:1.6.2" + implementation "com.patrykandpatrick.vico:compose-m3:1.6.2" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 47c3254..9373e78 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,7 +33,6 @@ android:hardwareAccelerated="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" - android:roundIcon="@mipmap/ic_launcher_round" android:theme="@style/Theme.Meshlinkkotlin"> > + get() = logsManager.logcatMessages val isAdvertising get() = bleManager.isAdvertising val service: BleService @@ -59,10 +63,15 @@ class BleService : Service() { bleManager.setUserId(id) } - fun sendMessage(recipientId: String, message: String, type: String = Message.Type.TEXT) { + fun sendMessage( + recipientId: String, + message: String, + type: String = Message.Type.TEXT + ): String { + val id = UUID.randomUUID().toString() this@BleService.sendMessage( Message( - UUID.randomUUID().toString(), + id, userId, recipientId, message, @@ -71,6 +80,7 @@ class BleService : Service() { true ) ) + return id } fun addDevice(userId: String) { @@ -177,6 +187,7 @@ class BleService : Service() { val messagesHashes: MutableMap> = mutableMapOf() val chunks: MutableMap>> = mutableMapOf() // userId -> messageId -> chunks + val sentMessages: MutableMap> = mutableMapOf() private val bleDataExchangeManager = object : BleManager.BleDataExchangeManager { override fun connect(address: String) { @@ -218,7 +229,8 @@ class BleService : Service() { name = null, blocked = false, publicKey = publicKey?.encoded?.let { Base64.encodeToString(it, Base64.DEFAULT) }, - added = false + added = false, + txPower = 0 ) ) } else { @@ -241,10 +253,15 @@ class BleService : Service() { } } - override fun onMessageSent(userId: String) { + override fun onMessageSent(userId: String, messageId: String) { val sharedPreferences = getSharedPreferences("USER_STATS", MODE_PRIVATE) - val sentMessages = sharedPreferences.getInt("TOTAL_MESSAGES_DELIVERED", 0) - sharedPreferences.edit().putInt("TOTAL_MESSAGES_DELIVERED", sentMessages + 1).apply() + val deliveredMessages = sharedPreferences.getInt("TOTAL_MESSAGES_DELIVERED", 0) + sharedPreferences.edit().putInt("TOTAL_MESSAGES_DELIVERED", deliveredMessages + 1).apply() + // Add the message to the sent messages list + val sentMessagesForUser = sentMessages[userId] ?: mutableListOf() + sentMessagesForUser.add(messageId) + sentMessages[userId] = sentMessagesForUser + // Remove the message from the chunks list Log.d("BleService", "onMessageSent: $userId") } @@ -258,9 +275,11 @@ class BleService : Service() { if (!bleManager.isConnected(address) && !bleManager.isConnecting(address)) { bleManager.connect(address) } - // is user blocked? val devices = allDevices?.value ?: emptyList() val device = devices.find { it.userId == getUserIdForAddress(address) } + if (device?.connected == false && bleManager.isConnected(address)) { + deviceRepository?.update(device.copy(connected = true)) + } if (device != null && device.blocked) { Log.d("BleService", "onChunkReceived: user is blocked") return @@ -387,7 +406,18 @@ class BleService : Service() { if (devices.isNotEmpty()) { devices.find { it.userId == userId }?.let { device -> if (device.rssi != rssi) { - deviceRepository?.update(device.copy(rssi = rssi)) + deviceRepository?.update(device.copy(rssi = rssi, connected = true)) + } + } + } + } + + override fun onUserTxPowerReceived(userId: String, txPower: Int) { + val devices = allDevices?.value ?: return + if (devices.isNotEmpty()) { + devices.find { it.userId == userId }?.let { device -> + if (device.txPower != txPower) { + deviceRepository?.update(device.copy(txPower = txPower, connected = true)) } } } @@ -415,6 +445,7 @@ class BleService : Service() { } lateinit var bleManager: BleManager private lateinit var encryptionManager: EncryptionManager + lateinit var logsManager: LogsManager var allMessages: LiveData>? = null var allDevices: LiveData>? = null @@ -433,6 +464,7 @@ class BleService : Service() { super.onCreate() handlerThread.start() handler = Handler(handlerThread.looper) + logsManager = LogsManager() encryptionManager = EncryptionManager() bleManager = BleManager(applicationContext, bleDataExchangeManager, encryptionManager, handler) } diff --git a/app/src/main/java/com/cstef/meshlink/MainActivity.kt b/app/src/main/java/com/cstef/meshlink/MainActivity.kt index 0e14371..cd969fe 100644 --- a/app/src/main/java/com/cstef/meshlink/MainActivity.kt +++ b/app/src/main/java/com/cstef/meshlink/MainActivity.kt @@ -123,51 +123,39 @@ class MainActivity : AppCompatActivity() { mutableStateOf(sharedPreferences.getBoolean("is_default_password", false)) } AnimatedNavHost(navController = navController, startDestination = "home") { - composable( - "home", - enterTransition = { - if (!isDatabaseOpening) { - slideInHorizontally( - initialOffsetX = { 1000 }, - animationSpec = tween(300) - ) + fadeIn(animationSpec = tween(300)) - } else { - fadeIn(animationSpec = tween(300)) - } - }, - exitTransition = { - if (!isDatabaseOpening) { - slideOutHorizontally( - targetOffsetX = { -1000 }, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) - } else { - null - } - }, - popEnterTransition = { + composable("home", enterTransition = { + if (!isDatabaseOpening) { slideInHorizontally( - initialOffsetX = { -1000 }, - animationSpec = tween(300) + initialOffsetX = { 1000 }, animationSpec = tween(300) ) + fadeIn(animationSpec = tween(300)) - }, - popExitTransition = { + } else { + fadeIn(animationSpec = tween(300)) + } + }, exitTransition = { + if (!isDatabaseOpening) { slideOutHorizontally( - targetOffsetX = { 1000 }, - animationSpec = tween(300) + targetOffsetX = { -1000 }, animationSpec = tween(300) ) + fadeOut(animationSpec = tween(300)) + } else { + null } - ) { + }, popEnterTransition = { + slideInHorizontally( + initialOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, popExitTransition = { + slideOutHorizontally( + targetOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }) { if (isDatabaseOpen) { Box(modifier = Modifier.fillMaxSize()) { bleBinder?.let { binder -> - HomeScreen( - allDevices = binder.allDevices, + HomeScreen(allDevices = binder.allDevices, userId = userId, onSelfClick = { navController.navigate("user/$userId") }, onDeviceLongClick = { navController.navigate("user/$it") }, - onDeviceSelected = { navController.navigate("chat/$it") } - ) + onDeviceSelected = { navController.navigate("chat/$it") }) FloatingActionButton( onClick = { navController.navigate("broadcast") }, modifier = Modifier @@ -183,8 +171,7 @@ class MainActivity : AppCompatActivity() { FloatingActionButton( onClick = { navController.navigate("add") - }, - modifier = Modifier + }, modifier = Modifier .align(Alignment.BottomEnd) .padding(24.dp) ) { @@ -214,38 +201,31 @@ class MainActivity : AppCompatActivity() { } } } - composable( - "chat/{deviceId}", + composable("chat/{deviceId}", arguments = listOf(navArgument("deviceId") { type = NavType.StringType }), enterTransition = { slideInHorizontally( - initialOffsetX = { 1000 }, - animationSpec = tween(300) + initialOffsetX = { 1000 }, animationSpec = tween(300) ) + fadeIn(animationSpec = tween(300)) }, exitTransition = { slideOutHorizontally( - targetOffsetX = { -1000 }, - animationSpec = tween(300) + targetOffsetX = { -1000 }, animationSpec = tween(300) ) + fadeOut(animationSpec = tween(300)) }, popEnterTransition = { slideInHorizontally( - initialOffsetX = { -1000 }, - animationSpec = tween(300) + initialOffsetX = { -1000 }, animationSpec = tween(300) ) + fadeIn(animationSpec = tween(300)) }, popExitTransition = { slideOutHorizontally( - targetOffsetX = { 1000 }, - animationSpec = tween(300) + targetOffsetX = { 1000 }, animationSpec = tween(300) ) + fadeOut(animationSpec = tween(300)) - } - ) { backStackEntry -> + }) { backStackEntry -> bleBinder?.let { binder -> val deviceId = backStackEntry.arguments?.getString("deviceId") ?: "" - ChatScreen( - deviceId = deviceId, + ChatScreen(deviceId = deviceId, allMessages = binder.allMessages, allDevices = binder.allDevices, sendMessage = { content, type -> @@ -256,77 +236,57 @@ class MainActivity : AppCompatActivity() { }) } } - composable( - "add", - enterTransition = { - slideInHorizontally( - initialOffsetX = { 1000 }, - animationSpec = tween(300) - ) + fadeIn(animationSpec = tween(300)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { -1000 }, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) - }, - popEnterTransition = { - slideInHorizontally( - initialOffsetX = { -1000 }, - animationSpec = tween(300) - ) + fadeIn(animationSpec = tween(300)) - }, - popExitTransition = { - slideOutHorizontally( - targetOffsetX = { 1000 }, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) - } - ) { + composable("add", enterTransition = { + slideInHorizontally( + initialOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, exitTransition = { + slideOutHorizontally( + targetOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }, popEnterTransition = { + slideInHorizontally( + initialOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, popExitTransition = { + slideOutHorizontally( + targetOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }) { bleBinder?.let { binder -> - AddDeviceScreen( - allDevices = binder.allDevices, - addDevice = { id -> - binder.addDevice(id) - }, - onBack = { user -> - if (user != null) { - navController.navigate("user/${user}") - } else { - navController.popBackStack() - } + AddDeviceScreen(allDevices = binder.allDevices, addDevice = { id -> + binder.addDevice(id) + }, onBack = { user -> + if (user != null) { + navController.navigate("user/${user}") + } else { + navController.popBackStack() } - ) + }) } } - composable( - "user/{deviceId}", + composable("user/{deviceId}", arguments = listOf(navArgument("deviceId") { type = NavType.StringType }), enterTransition = { slideInHorizontally( - initialOffsetX = { 1000 }, - animationSpec = tween(300) + initialOffsetX = { 1000 }, animationSpec = tween(300) ) + fadeIn(animationSpec = tween(300)) }, exitTransition = { slideOutHorizontally( - targetOffsetX = { -1000 }, - animationSpec = tween(300) + targetOffsetX = { -1000 }, animationSpec = tween(300) ) + fadeOut(animationSpec = tween(300)) }, popEnterTransition = { slideInHorizontally( - initialOffsetX = { -1000 }, - animationSpec = tween(300) + initialOffsetX = { -1000 }, animationSpec = tween(300) ) + fadeIn(animationSpec = tween(300)) }, popExitTransition = { slideOutHorizontally( - targetOffsetX = { 1000 }, - animationSpec = tween(300) + targetOffsetX = { 1000 }, animationSpec = tween(300) ) + fadeOut(animationSpec = tween(300)) - } - ) { backStackEntry -> + }) { backStackEntry -> // User info screen val otherUserId = backStackEntry.arguments?.getString("deviceId") ?: "" bleBinder?.let { binder -> @@ -356,33 +316,23 @@ class MainActivity : AppCompatActivity() { ) } } - composable( - "settings", - enterTransition = { - slideInHorizontally( - initialOffsetX = { 1000 }, - animationSpec = tween(300) - ) + fadeIn(animationSpec = tween(300)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { -1000 }, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) - }, - popEnterTransition = { - slideInHorizontally( - initialOffsetX = { -1000 }, - animationSpec = tween(300) - ) + fadeIn(animationSpec = tween(300)) - }, - popExitTransition = { - slideOutHorizontally( - targetOffsetX = { 1000 }, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) - } - ) { + composable("settings", enterTransition = { + slideInHorizontally( + initialOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, exitTransition = { + slideOutHorizontally( + targetOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }, popEnterTransition = { + slideInHorizontally( + initialOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, popExitTransition = { + slideOutHorizontally( + targetOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }) { bleBinder?.let { binder -> SettingsScreen( isAdvertising = binder.isAdvertising, @@ -398,6 +348,12 @@ class MainActivity : AppCompatActivity() { goToStats = { navController.navigate("stats") }, + goToLogs = { + navController.navigate("logs") + }, + goToBenchmark = { + navController.navigate("benchmark") + }, deleteAllData = { binder.deleteAllData() }, @@ -407,62 +363,42 @@ class MainActivity : AppCompatActivity() { ) } } - composable( - "about", - enterTransition = { - slideInHorizontally( - initialOffsetX = { 1000 }, - animationSpec = tween(300) - ) + fadeIn(animationSpec = tween(300)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { -1000 }, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) - }, - popEnterTransition = { - slideInHorizontally( - initialOffsetX = { -1000 }, - animationSpec = tween(300) - ) + fadeIn(animationSpec = tween(300)) - }, - popExitTransition = { - slideOutHorizontally( - targetOffsetX = { 1000 }, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) - } - ) { + composable("about", enterTransition = { + slideInHorizontally( + initialOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, exitTransition = { + slideOutHorizontally( + targetOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }, popEnterTransition = { + slideInHorizontally( + initialOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, popExitTransition = { + slideOutHorizontally( + targetOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }) { AboutScreen() } - composable( - "password", - enterTransition = { - slideInHorizontally( - initialOffsetX = { 1000 }, - animationSpec = tween(300) - ) + fadeIn(animationSpec = tween(300)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { -1000 }, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) - }, - popEnterTransition = { - slideInHorizontally( - initialOffsetX = { -1000 }, - animationSpec = tween(300) - ) + fadeIn(animationSpec = tween(300)) - }, - popExitTransition = { - slideOutHorizontally( - targetOffsetX = { 1000 }, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) - } - ) { + composable("password", enterTransition = { + slideInHorizontally( + initialOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, exitTransition = { + slideOutHorizontally( + targetOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }, popEnterTransition = { + slideInHorizontally( + initialOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, popExitTransition = { + slideOutHorizontally( + targetOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }) { bleBinder?.let { binder -> PasswordScreen( firstTime = firstTime, @@ -473,32 +409,23 @@ class MainActivity : AppCompatActivity() { ) } } - composable("broadcast", - enterTransition = { - slideInHorizontally( - initialOffsetX = { 1000 }, - animationSpec = tween(300) - ) + fadeIn(animationSpec = tween(300)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { -1000 }, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) - }, - popEnterTransition = { - slideInHorizontally( - initialOffsetX = { -1000 }, - animationSpec = tween(300) - ) + fadeIn(animationSpec = tween(300)) - }, - popExitTransition = { - slideOutHorizontally( - targetOffsetX = { 1000 }, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) - } - ) { + composable("broadcast", enterTransition = { + slideInHorizontally( + initialOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, exitTransition = { + slideOutHorizontally( + targetOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }, popEnterTransition = { + slideInHorizontally( + initialOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, popExitTransition = { + slideOutHorizontally( + targetOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }) { bleBinder?.let { binder -> BroadcastScreen( allMessages = binder.allMessages, @@ -512,34 +439,73 @@ class MainActivity : AppCompatActivity() { ) } } - composable("stats", - enterTransition = { - slideInHorizontally( - initialOffsetX = { 1000 }, - animationSpec = tween(300) - ) + fadeIn(animationSpec = tween(300)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { -1000 }, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) - }, - popEnterTransition = { - slideInHorizontally( - initialOffsetX = { -1000 }, - animationSpec = tween(300) - ) + fadeIn(animationSpec = tween(300)) - }, - popExitTransition = { - slideOutHorizontally( - targetOffsetX = { 1000 }, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) - } - ) { + composable("stats", enterTransition = { + slideInHorizontally( + initialOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, exitTransition = { + slideOutHorizontally( + targetOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }, popEnterTransition = { + slideInHorizontally( + initialOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, popExitTransition = { + slideOutHorizontally( + targetOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }) { StatsScreen() } + composable("logs", enterTransition = { + slideInHorizontally( + initialOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, exitTransition = { + slideOutHorizontally( + targetOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }, popEnterTransition = { + slideInHorizontally( + initialOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, popExitTransition = { + slideOutHorizontally( + targetOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }) { + bleBinder?.let { binder -> + LogsScreen( + logcatLogs = binder.logcatLogs, + ) { + binder.service.logsManager.logcatMessages.value = emptyList() + } + } + } + composable("benchmark", enterTransition = { + slideInHorizontally( + initialOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, exitTransition = { + slideOutHorizontally( + targetOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }, popEnterTransition = { + slideInHorizontally( + initialOffsetX = { -1000 }, animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) + }, popExitTransition = { + slideOutHorizontally( + targetOffsetX = { 1000 }, animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) + }) { + bleBinder?.let { binder -> + BenchmarkScreen( + binder = binder, + ) + } + } } LaunchedEffect(databaseError) { if (databaseError.isNotEmpty()) { @@ -562,40 +528,36 @@ class MainActivity : AppCompatActivity() { popUpTo("home") { inclusive = true } } } - RequestMultiplePermissions( - permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - listOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.BLUETOOTH, - Manifest.permission.BLUETOOTH_ADVERTISE, - Manifest.permission.BLUETOOTH_CONNECT, - Manifest.permission.BLUETOOTH_SCAN, - ) - } else { - listOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.BLUETOOTH, - Manifest.permission.BLUETOOTH_ADMIN, - ) - }, - content = { - LaunchedEffect(Unit) { - if (sharedPreferences.getBoolean("first_time", true)) { - val editor = sharedPreferences.edit() - editor.putBoolean("first_time", false) - editor.apply() - } - if (!checkBluetoothEnabled()) { - Toast.makeText(this@MainActivity, "Bluetooth is disabled", Toast.LENGTH_LONG) - .show() - } else { - start() - } + RequestMultiplePermissions(permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + listOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_ADVERTISE, + Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.BLUETOOTH_SCAN, + ) + } else { + listOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_ADMIN, + ) + }, content = { + LaunchedEffect(Unit) { + if (sharedPreferences.getBoolean("first_time", true)) { + val editor = sharedPreferences.edit() + editor.putBoolean("first_time", false) + editor.apply() + } + if (!checkBluetoothEnabled()) { + Toast.makeText(this@MainActivity, "Bluetooth is disabled", Toast.LENGTH_LONG).show() + } else { + start() } } - ) + }) } } } diff --git a/app/src/main/java/com/cstef/meshlink/db/Database.kt b/app/src/main/java/com/cstef/meshlink/db/Database.kt index f05f149..3f42733 100644 --- a/app/src/main/java/com/cstef/meshlink/db/Database.kt +++ b/app/src/main/java/com/cstef/meshlink/db/Database.kt @@ -16,7 +16,7 @@ import net.sqlcipher.database.SupportFactory Device::class, Message::class, ], - version = 16, + version = 17, exportSchema = false, ) abstract class AppDatabase : RoomDatabase() { diff --git a/app/src/main/java/com/cstef/meshlink/db/entities/Device.kt b/app/src/main/java/com/cstef/meshlink/db/entities/Device.kt index 195e7db..8d2a3b4 100644 --- a/app/src/main/java/com/cstef/meshlink/db/entities/Device.kt +++ b/app/src/main/java/com/cstef/meshlink/db/entities/Device.kt @@ -15,4 +15,5 @@ data class Device( @ColumnInfo(name = "blocked") val blocked: Boolean = false, @ColumnInfo(name = "public_key") val publicKey: String? = null, @ColumnInfo(name = "added") val added: Boolean = false, + @ColumnInfo(name = "tx_power") val txPower: Int = 0, ) diff --git a/app/src/main/java/com/cstef/meshlink/managers/BleManager.kt b/app/src/main/java/com/cstef/meshlink/managers/BleManager.kt index 7149e78..75079c6 100644 --- a/app/src/main/java/com/cstef/meshlink/managers/BleManager.kt +++ b/app/src/main/java/com/cstef/meshlink/managers/BleManager.kt @@ -31,8 +31,6 @@ class BleManager( val isAdvertising = mutableStateOf(false) val isStarted = mutableStateOf(false) - val isScanning = mutableStateOf(false) - private val tag = BleManager::class.java.canonicalName private val adapter get() = (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter // BLE Callbacks are executed on a binder thread. This handlers gets the work @@ -61,7 +59,7 @@ class BleManager( @SuppressLint("MissingPermission") fun stop() { - Log.d(tag, "BleManager stopped") + Log.d("BleManager", "BleManager stopped") if (canBeClient) clientManager.stop() if (canBeServer) serverManager.stopAdvertising() isStarted.value = false @@ -70,15 +68,15 @@ class BleManager( fun sendMessage(message: Message) { val deviceAddress = clientManager.connectedServersAddresses[message.recipientId] if (deviceAddress != null && clientManager.connectedGattServers.containsKey(deviceAddress)) { - Log.d(tag, clientManager.connectedGattServers[deviceAddress]?.device?.address!!) - Log.d(tag, "Sending data to ${message.recipientId}") + Log.d("BleManager", clientManager.connectedGattServers[deviceAddress]?.device?.address!!) + Log.d("BleManager", "Sending data to ${message.recipientId}") clientManager.sendData(message) } else { - Log.d(tag, "No connection to ${message.recipientId}, broadcasting data") + Log.d("BleManager", "No connection to ${message.recipientId}, broadcasting data") if (message.type == Message.Type.TEXT) { clientManager.broadcastData(message) } else { - Log.e(tag, "Cannot broadcast other than text messages") + Log.e("BleManager", "Cannot broadcast other than text messages") } } } @@ -87,13 +85,13 @@ class BleManager( if (message.type == Message.Type.TEXT) { clientManager.broadcastData(message) } else { - Log.e(tag, "Cannot broadcast other than text messages") + Log.e("BleManager", "Cannot broadcast other than text messages") } } fun getUserIdForAddress(address: String): String? { Log.d( - tag, + "BleManager", "Getting user id for address $address, connected servers: ${clientManager.connectedServersAddresses}" ) return clientManager.connectedServersAddresses.entries.find { it.value == address }?.key @@ -174,7 +172,7 @@ class BleManager( fun getUsername(): String = "" fun getPublicKeyForUser(recipientId: String): PublicKey? fun onUserRssiReceived(userId: String, rssi: Int) {} - fun onMessageSent(userId: String) {} + fun onMessageSent(userId: String, messageId: String) {} fun onUserWriting(userId: String, isWriting: Boolean) {} fun getUserIdForAddress(address: String): String? = "" fun onMessageSendFailed(userId: String?, reason: String?) {} @@ -182,5 +180,6 @@ class BleManager( fun getAddressForUserId(userId: String): String = "" fun onUserAdded(userId: String) fun connect(address: String) {} + fun onUserTxPowerReceived(userId: String, txPower: Int) {} } } diff --git a/app/src/main/java/com/cstef/meshlink/managers/ClientBleManager.kt b/app/src/main/java/com/cstef/meshlink/managers/ClientBleManager.kt index a4d7f95..cb2cdda 100644 --- a/app/src/main/java/com/cstef/meshlink/managers/ClientBleManager.kt +++ b/app/src/main/java/com/cstef/meshlink/managers/ClientBleManager.kt @@ -63,7 +63,8 @@ class ClientBleManager( val userId: String, val notificationId: Int, val startTime: Long = System.currentTimeMillis(), - val receiver: BroadcastReceiver + val receiver: BroadcastReceiver, + val messageId: String ) private val sendingChunks = mutableMapOf() @@ -72,10 +73,14 @@ class ClientBleManager( private val scanner get() = adapter?.bluetoothLeScanner private val scanFilters = - ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(BleUuid.SERVICE_UUID)).build() + ScanFilter.Builder() + .setServiceUuid(ParcelUuid.fromString(BleUuid.SERVICE_UUID)) + // .setManufacturerData(0xDEAD, byteArrayOf(0x00)) + .build() .let { listOf(it) } - private val scanSettings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) + private val scanSettings = ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_BALANCED) .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE).build() @@ -94,6 +99,18 @@ class ClientBleManager( } } } + result.txPower.let { txPower -> + if (txPower == ScanResult.TX_POWER_NOT_PRESENT) { + Log.d("ClientBleManager", "onScanResult: txPower not present") + return@execute + } + connectedServersAddresses.entries.find { it.value == device.address }?.key.let { userId -> +// Log.d("ClientBleManager", "onScanResult: $userId $txPower") + if (userId != null) { + dataExchangeManager.onUserTxPowerReceived(userId, txPower) + } + } + } operationQueue.operationComplete() return@execute } else { @@ -180,6 +197,16 @@ class ClientBleManager( val serverId = connectedServersAddresses.entries.find { it.value == gatt.device.address }?.key connectedServersAddresses.remove(serverId) + if (sendingChunks.containsKey(gatt.device.address)) { + sendingChunks.remove(gatt.device.address) + val chunkSendingState = sendingChunks[gatt.device.address] + val notificationId = chunkSendingState?.notificationId ?: serverId.hashCode() + NotificationManagerCompat.from(context).cancel(notificationId) + dataExchangeManager.onMessageSendFailed(serverId, "Connection error") + } + if (serverId != null) { + dataExchangeManager.onUserDisconnected(serverId) + } gatt.close() } } @@ -257,12 +284,14 @@ class ClientBleManager( .generatePublic(X509EncodedKeySpec(Base64.decode(msg.key, Base64.DEFAULT))) Log.d( "ClientBleManager", - "onCharacteristicRead: publicKey = $publicKey gatt == null: ${gatt == null}" + "onCharacteristicRead: publicKey = ${encryptionManager.getPublicKeySignature(publicKey)} gatt == null: ${gatt == null}" ) if (gatt != null) { callbackHandler.post { dataExchangeManager.onUserConnected( - msg.userId, gatt.device.address, publicKey + userId = msg.userId, + address = gatt.device.address, + publicKey = publicKey ) } } else { @@ -308,7 +337,7 @@ class ClientBleManager( notificationManager.cancel(chunkState.notificationId) sendingChunks.remove(gatt?.device?.address) callbackHandler.post { - dataExchangeManager.onMessageSent(chunkState.userId) + dataExchangeManager.onMessageSent(chunkState.userId, chunkState.messageId) } } else { val timeDiff = System.currentTimeMillis() - chunkState.startTime @@ -357,6 +386,7 @@ class ClientBleManager( } } else { Log.e("ClientBleManager", "onReadRemoteRssi: userId == null: ${gatt?.device?.address}") + gatt?.disconnect() } } else { Log.e("ClientBleManager", "onReadRemoteRssi: failed to read rssi") @@ -455,7 +485,8 @@ class ClientBleManager( userId = message.recipientId ?: "", notificationId = notificationId, startTime = System.currentTimeMillis(), - receiver = receiver + receiver = receiver, + messageId = message.id ) context.registerReceiver(receiver, IntentFilter("cancel_sending")) chunks.forEachIndexed { index, chunk -> diff --git a/app/src/main/java/com/cstef/meshlink/managers/LogsManager.kt b/app/src/main/java/com/cstef/meshlink/managers/LogsManager.kt new file mode 100644 index 0000000..146ca93 --- /dev/null +++ b/app/src/main/java/com/cstef/meshlink/managers/LogsManager.kt @@ -0,0 +1,101 @@ +package com.cstef.meshlink.managers + +import android.icu.util.GregorianCalendar +import androidx.lifecycle.MutableLiveData +import java.io.BufferedReader +import java.io.InputStreamReader +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +// A class that listens to logcat on a separate thread and formats them into a LogcatMessage object + +data class LogcatMessage( + val pid: String, + val tid: String, + val priority: String, + val tag: String, + val message: String, + val time: Long +) + +class LogsManager { + val logcatMessages = MutableLiveData>() + private val logcatMessagesLock = ReentrantLock() + private val logcatMessagesCondition = logcatMessagesLock.newCondition() + private val logcatMessagesThread = Thread { + // Clear the logcat + Runtime.getRuntime().exec("logcat -c").waitFor() + val process = Runtime.getRuntime().exec("logcat -v threadtime") + val reader = BufferedReader(InputStreamReader(process.inputStream)) + var line: String? + while (true) { + line = reader.readLine() + if (line != null) { + val logcatMessage = parseLogcatMessage(line) + if (logcatMessage != null) { + // if the tag is included in the list of tags not to be displayed, skip it + val tagsToSkip = + listOf( + "Quality", + "AutofillManager", + "InputMethodManager", + "InsetsController", + "ProfileInstaller", + "CompatibilityChangeReporter", + "Activity", + "Choreographer", + "Looper", + "OpenGLRenderer", + "OplusSystemUINavigationGesture", + "System", + "BLASTBufferQueue", + ) + if (logcatMessage.tag !in tagsToSkip) { + logcatMessagesLock.withLock { + logcatMessages.postValue( + logcatMessages.value?.plus(logcatMessage) ?: listOf( + logcatMessage + ) + ) + logcatMessagesCondition.signal() + } + } + } + } + } + } + + init { + logcatMessagesThread.start() + } + + private fun parseLogcatMessage(line: String): LogcatMessage? { + val regex = Regex("([0-9-]+ [0-9:.]+) +([0-9]+) +([0-9]+) +([A-Z]) +([^:]+): (.*)") + val matchResult = regex.find(line) + if (matchResult != null) { + val groups = matchResult.groups + val time = GregorianCalendar().apply { + val date = groups[1]!!.value.split(" ") + val dateParts = date[0].split("-") + val timeParts = date[1].split(":") + set( + /* Current year */ get(GregorianCalendar.YEAR), + dateParts[0].toInt(), + dateParts[1].toInt(), + timeParts[0].toInt(), + timeParts[1].toInt(), + timeParts[2].split(".")[0].toInt() + ) + }.timeInMillis + return LogcatMessage( + pid = groups[2]!!.value.trim(), + tid = groups[3]!!.value.trim(), + priority = groups[4]!!.value.trim(), + tag = groups[5]!!.value.trim(), + message = groups[6]!!.value.trim(), + time = time + ) + } + return null + } +} diff --git a/app/src/main/java/com/cstef/meshlink/managers/ServerBleManager.kt b/app/src/main/java/com/cstef/meshlink/managers/ServerBleManager.kt index fce48c0..711792e 100644 --- a/app/src/main/java/com/cstef/meshlink/managers/ServerBleManager.kt +++ b/app/src/main/java/com/cstef/meshlink/managers/ServerBleManager.kt @@ -191,11 +191,12 @@ class ServerBleManager( callbackHandler.post { device?.address?.let { dataExchangeManager.connect(it) } } - } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { - callbackHandler.post { - dataExchangeManager.onUserDisconnected(device?.address!!) - } } +// else if (newState == BluetoothProfile.STATE_DISCONNECTED) { +// callbackHandler.post { +// dataExchangeManager.onUserDisconnected(device?.address!!) +// } +// } } } @@ -223,6 +224,7 @@ class ServerBleManager( .addServiceUuid(ParcelUuid.fromString(BleUuid.SERVICE_UUID)) .setIncludeDeviceName(false) .setIncludeTxPowerLevel(false) + //.addManufacturerData(0xDEAD, byteArrayOf(0x00)) .build() Log.d( "ServerBleManager", diff --git a/app/src/main/java/com/cstef/meshlink/screens/BenchmarkScreen.kt b/app/src/main/java/com/cstef/meshlink/screens/BenchmarkScreen.kt new file mode 100644 index 0000000..316f1a2 --- /dev/null +++ b/app/src/main/java/com/cstef/meshlink/screens/BenchmarkScreen.kt @@ -0,0 +1,64 @@ +package com.cstef.meshlink.screens + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.cstef.meshlink.BleService +import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis +import com.patrykandpatrick.vico.compose.axis.vertical.startAxis +import com.patrykandpatrick.vico.compose.chart.Chart +import com.patrykandpatrick.vico.compose.chart.line.lineChart +import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer +import com.patrykandpatrick.vico.core.entry.FloatEntry + +@Composable +fun BenchmarkScreen(binder: BleService.BleServiceBinder) { + val benchmarkResults = remember { mutableStateOf(emptyList()) } + val benchmarkRunning = remember { + mutableStateOf(false) + } + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Button( + onClick = { + benchmarkRunning.value = true + benchmarkResults.value = benchmark(binder) + benchmarkRunning.value = false + }, + enabled = !benchmarkRunning.value, + content = { + Text("Run Benchmark") + } + ) + if (benchmarkResults.value.isNotEmpty()) { + val producer = ChartEntryModelProducer(benchmarkResults.value.mapIndexed { index, value -> + FloatEntry( + index.toFloat(), + value.toFloat() + ) + }) + Chart( + chart = lineChart(), + chartModelProducer = producer, + modifier = Modifier + .fillMaxWidth() + .height(300.dp), + startAxis = startAxis(), + bottomAxis = bottomAxis(), + ) + } + } +} + +fun benchmark(binder: BleService.BleServiceBinder): List { + return emptyList() +} diff --git a/app/src/main/java/com/cstef/meshlink/screens/LogsScreen.kt b/app/src/main/java/com/cstef/meshlink/screens/LogsScreen.kt new file mode 100644 index 0000000..2879e03 --- /dev/null +++ b/app/src/main/java/com/cstef/meshlink/screens/LogsScreen.kt @@ -0,0 +1,113 @@ +package com.cstef.meshlink.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ClearAll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.MutableLiveData +import com.cstef.meshlink.managers.LogcatMessage + +@Composable +fun LogsScreen(logcatLogs: MutableLiveData>, clearLogs: () -> Unit) { + val logs by logcatLogs.observeAsState(listOf()) + LogsList(logs = logs, clearLogs = clearLogs) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LogsList(logs: List, clearLogs: () -> Unit) { + val colors = MaterialTheme.colorScheme + // go to the bottom of the list when new logs are added + val scrollState = rememberScrollState() + LaunchedEffect(logs.size) { + scrollState.animateScrollTo(logs.size * 100) + } + val (filter, setFilter) = remember { mutableStateOf("") } + Column(modifier = Modifier.fillMaxSize()) { + TopAppBar( + title = { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + OutlinedTextField( + value = filter, + onValueChange = setFilter, + label = { Text("Filter") }, + modifier = Modifier + .weight(1f) + .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp) + .align(Alignment.CenterVertically), + singleLine = true, + textStyle = MaterialTheme.typography.bodyMedium, + ) + IconButton( + onClick = { + clearLogs() + }, + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp) + .align(Alignment.CenterVertically) + ) { + Icon( + imageVector = Icons.Default.ClearAll, + contentDescription = "Clear logs", + tint = colors.onSurface + ) + } + } + }, + modifier = Modifier.height(96.dp), + colors = TopAppBarDefaults.topAppBarColors( + containerColor = colors.background, + ), + ) + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + items(logs.filter { + it.message.contains(filter, true) || it.tag.contains( + filter, true + ) + }) { log -> + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(Color.Gray) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = log.tag, modifier = Modifier.padding(start = 8.dp), color = when (log.priority) { + "V" -> colors.onSurface + "D" -> colors.primary + "I" -> colors.secondary + "W" -> colors.tertiary + "E" -> colors.error + else -> colors.onSurface + }, style = MaterialTheme.typography.bodyMedium + ) + Text( + text = log.message, + modifier = Modifier.padding(start = 8.dp), + color = colors.onBackground, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } +} diff --git a/app/src/main/java/com/cstef/meshlink/screens/SettingsScreen.kt b/app/src/main/java/com/cstef/meshlink/screens/SettingsScreen.kt index abb8127..0287b62 100644 --- a/app/src/main/java/com/cstef/meshlink/screens/SettingsScreen.kt +++ b/app/src/main/java/com/cstef/meshlink/screens/SettingsScreen.kt @@ -4,6 +4,7 @@ import android.widget.Toast import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Feed import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -19,6 +20,8 @@ fun SettingsScreen( stopAdvertising: () -> Unit, goToAbout: () -> Unit, goToStats: () -> Unit, + goToLogs: () -> Unit, + goToBenchmark: () -> Unit, deleteAllData: () -> Unit, changeDatabasePassword: (password: String) -> Boolean, ) { @@ -30,6 +33,30 @@ fun SettingsScreen( modifier = Modifier .height(56.dp) .padding(top = 16.dp), + actions = { + Row( + modifier = Modifier + .fillMaxHeight() + .padding(end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + IconButton(onClick = goToLogs) { + Icon( + imageVector = Icons.Filled.Feed, + contentDescription = "Go to logs screen", + tint = colors.onSurface + ) + } +// IconButton(onClick = goToBenchmark) { +// Icon( +// imageVector = Icons.Outlined.TrendingUp, +// contentDescription = "Go to benchmark screen", +// tint = colors.onSurface +// ) +// } + } + } ) Column { Column( diff --git a/app/src/main/java/com/cstef/meshlink/screens/UserInfoScreen.kt b/app/src/main/java/com/cstef/meshlink/screens/UserInfoScreen.kt index 7f06daf..c6c1d38 100644 --- a/app/src/main/java/com/cstef/meshlink/screens/UserInfoScreen.kt +++ b/app/src/main/java/com/cstef/meshlink/screens/UserInfoScreen.kt @@ -1,5 +1,6 @@ package com.cstef.meshlink.screens +import android.util.Log import android.widget.Toast import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -18,6 +19,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.LiveData import com.cstef.meshlink.db.entities.Device import com.cstef.meshlink.ui.components.DeviceID +import kotlin.math.pow @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -65,6 +67,19 @@ fun UserInfoScreen( color = colors.onBackground ) } + // Formula: Distance = 10 ^ ((Measured Power - RSSI)/(10 * N)) where N = 2 (in free space) and N = 3 (in walls), Measured Power = -50 (at 1 meter) + if (device?.connected == true && device.rssi != 0) { + Log.d("UserInfoScreen", "RSSI: ${device.rssi}, TX Power: ${device.txPower}") + val distance = 10.0.pow(((-59.0) - device.rssi.toDouble()) / (10.0 * 3.0)) + Text( + text = "~%.2f meters away".format(distance), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .padding(bottom = 16.dp) + .align(Alignment.CenterHorizontally), + color = colors.onBackground + ) + } publicKey?.let { Text( text = "Public key", diff --git a/app/src/main/java/com/cstef/meshlink/ui/components/AddedDevice.kt b/app/src/main/java/com/cstef/meshlink/ui/components/AddedDevice.kt index 8e0b29d..d03d452 100644 --- a/app/src/main/java/com/cstef/meshlink/ui/components/AddedDevice.kt +++ b/app/src/main/java/com/cstef/meshlink/ui/components/AddedDevice.kt @@ -70,8 +70,8 @@ fun AddedDevice( Icon( imageVector = if (!device.connected) Icons.Rounded.SignalWifiOff - else if (device.rssi == 0) Icons.Rounded.SignalWifiBad - else if (device.rssi >= -60) Icons.Rounded.NetworkWifi + else if (device.rssi == 0) Icons.Rounded.QuestionMark + else if (device.rssi >= -60) Icons.Rounded.SignalWifi4Bar else if (device.rssi >= -70) Icons.Rounded.NetworkWifi3Bar else if (device.rssi >= -80) Icons.Rounded.NetworkWifi2Bar else if (device.rssi >= -90) Icons.Rounded.NetworkWifi1Bar diff --git a/app/src/main/java/com/cstef/meshlink/ui/components/TextCard.kt b/app/src/main/java/com/cstef/meshlink/ui/components/TextCard.kt index fa331f5..78a000b 100644 --- a/app/src/main/java/com/cstef/meshlink/ui/components/TextCard.kt +++ b/app/src/main/java/com/cstef/meshlink/ui/components/TextCard.kt @@ -32,6 +32,7 @@ fun TextCard( } else { MaterialTheme.colorScheme.secondary }, + contentColor = MaterialTheme.colorScheme.onSecondary ) ) { Row { @@ -41,7 +42,10 @@ fun TextCard( .fillMaxWidth() .align(alignment = Alignment.CenterVertically) ) { - Text(text = content, style = MaterialTheme.typography.bodyLarge) + Text( + text = content, + style = MaterialTheme.typography.bodyLarge, + ) } } } diff --git a/app/src/main/java/com/cstef/meshlink/ui/theme/Color.kt b/app/src/main/java/com/cstef/meshlink/ui/theme/Color.kt index 28403f4..de6a4fd 100755 --- a/app/src/main/java/com/cstef/meshlink/ui/theme/Color.kt +++ b/app/src/main/java/com/cstef/meshlink/ui/theme/Color.kt @@ -1,67 +1,67 @@ package com.cstef.meshlink.ui.theme import androidx.compose.ui.graphics.Color -val md_theme_light_primary = Color(0xFF6750A4) +val md_theme_light_primary = Color(0xFF3950D2) val md_theme_light_onPrimary = Color(0xFFFFFFFF) -val md_theme_light_primaryContainer = Color(0xFFEADDFF) -val md_theme_light_onPrimaryContainer = Color(0xFF21005D) -val md_theme_light_secondary = Color(0xFF625B71) +val md_theme_light_primaryContainer = Color(0xFFDEE0FF) +val md_theme_light_onPrimaryContainer = Color(0xFF000F5D) +val md_theme_light_secondary = Color(0xFF5B5D72) val md_theme_light_onSecondary = Color(0xFFFFFFFF) -val md_theme_light_secondaryContainer = Color(0xFFE8DEF8) -val md_theme_light_onSecondaryContainer = Color(0xFF1D192B) -val md_theme_light_tertiary = Color(0xFF7D5260) +val md_theme_light_secondaryContainer = Color(0xFFE0E1F9) +val md_theme_light_onSecondaryContainer = Color(0xFF181A2C) +val md_theme_light_tertiary = Color(0xFF77536D) val md_theme_light_onTertiary = Color(0xFFFFFFFF) -val md_theme_light_tertiaryContainer = Color(0xFFFFD8E4) -val md_theme_light_onTertiaryContainer = Color(0xFF31111D) -val md_theme_light_error = Color(0xFFB3261E) +val md_theme_light_tertiaryContainer = Color(0xFFFFD7F1) +val md_theme_light_onTertiaryContainer = Color(0xFF2D1228) +val md_theme_light_error = Color(0xFFBA1A1A) +val md_theme_light_errorContainer = Color(0xFFFFDAD6) val md_theme_light_onError = Color(0xFFFFFFFF) -val md_theme_light_errorContainer = Color(0xFFF9DEDC) -val md_theme_light_onErrorContainer = Color(0xFF410E0B) -val md_theme_light_outline = Color(0xFF79747E) -val md_theme_light_background = Color(0xFFFFFBFE) -val md_theme_light_onBackground = Color(0xFF1C1B1F) -val md_theme_light_surface = Color(0xFFFFFBFE) -val md_theme_light_onSurface = Color(0xFF1C1B1F) -val md_theme_light_surfaceVariant = Color(0xFFE7E0EC) -val md_theme_light_onSurfaceVariant = Color(0xFF49454F) -val md_theme_light_inverseSurface = Color(0xFF313033) -val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4) -val md_theme_light_inversePrimary = Color(0xFFD0BCFF) +val md_theme_light_onErrorContainer = Color(0xFF410002) +val md_theme_light_background = Color(0xFFFFFBFF) +val md_theme_light_onBackground = Color(0xFF1B1B1F) +val md_theme_light_surface = Color(0xFFFFFBFF) +val md_theme_light_onSurface = Color(0xFF1B1B1F) +val md_theme_light_surfaceVariant = Color(0xFFE3E1EC) +val md_theme_light_onSurfaceVariant = Color(0xFF46464F) +val md_theme_light_outline = Color(0xFF767680) +val md_theme_light_inverseOnSurface = Color(0xFFF3F0F4) +val md_theme_light_inverseSurface = Color(0xFF303034) +val md_theme_light_inversePrimary = Color(0xFFBBC3FF) val md_theme_light_shadow = Color(0xFF000000) -val md_theme_light_surfaceTint = Color(0xFF6750A4) -val md_theme_light_outlineVariant = Color(0xFFCAC4D0) +val md_theme_light_surfaceTint = Color(0xFF3950D2) +val md_theme_light_outlineVariant = Color(0xFFC7C5D0) val md_theme_light_scrim = Color(0xFF000000) -val md_theme_dark_primary = Color(0xFFD0BCFF) -val md_theme_dark_onPrimary = Color(0xFF381E72) -val md_theme_dark_primaryContainer = Color(0xFF4F378B) -val md_theme_dark_onPrimaryContainer = Color(0xFFEADDFF) -val md_theme_dark_secondary = Color(0xFFCCC2DC) -val md_theme_dark_onSecondary = Color(0xFF332D41) -val md_theme_dark_secondaryContainer = Color(0xFF4A4458) -val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8) -val md_theme_dark_tertiary = Color(0xFFEFB8C8) -val md_theme_dark_onTertiary = Color(0xFF492532) -val md_theme_dark_tertiaryContainer = Color(0xFF633B48) -val md_theme_dark_onTertiaryContainer = Color(0xFFFFD8E4) -val md_theme_dark_error = Color(0xFFF2B8B5) -val md_theme_dark_onError = Color(0xFF601410) -val md_theme_dark_errorContainer = Color(0xFF8C1D18) -val md_theme_dark_onErrorContainer = Color(0xFFF9DEDC) -val md_theme_dark_outline = Color(0xFF938F99) -val md_theme_dark_background = Color(0xFF1C1B1F) -val md_theme_dark_onBackground = Color(0xFFE6E1E5) -val md_theme_dark_surface = Color(0xFF1C1B1F) -val md_theme_dark_onSurface = Color(0xFFE6E1E5) -val md_theme_dark_surfaceVariant = Color(0xFF49454F) -val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4D0) -val md_theme_dark_inverseSurface = Color(0xFFE6E1E5) -val md_theme_dark_inverseOnSurface = Color(0xFF313033) -val md_theme_dark_inversePrimary = Color(0xFF6750A4) +val md_theme_dark_primary = Color(0xFFBBC3FF) +val md_theme_dark_onPrimary = Color(0xFF001D92) +val md_theme_dark_primaryContainer = Color(0xFF1934BA) +val md_theme_dark_onPrimaryContainer = Color(0xFFDEE0FF) +val md_theme_dark_secondary = Color(0xFFC3C5DD) +val md_theme_dark_onSecondary = Color(0xFF2D2F42) +val md_theme_dark_secondaryContainer = Color(0xFF434559) +val md_theme_dark_onSecondaryContainer = Color(0xFFE0E1F9) +val md_theme_dark_tertiary = Color(0xFFE6BAD7) +val md_theme_dark_onTertiary = Color(0xFF44263D) +val md_theme_dark_tertiaryContainer = Color(0xFF5D3C54) +val md_theme_dark_onTertiaryContainer = Color(0xFFFFD7F1) +val md_theme_dark_error = Color(0xFFFFB4AB) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF1B1B1F) +val md_theme_dark_onBackground = Color(0xFFE4E1E6) +val md_theme_dark_surface = Color(0xFF1B1B1F) +val md_theme_dark_onSurface = Color(0xFFE4E1E6) +val md_theme_dark_surfaceVariant = Color(0xFF46464F) +val md_theme_dark_onSurfaceVariant = Color(0xFFC7C5D0) +val md_theme_dark_outline = Color(0xFF90909A) +val md_theme_dark_inverseOnSurface = Color(0xFF1B1B1F) +val md_theme_dark_inverseSurface = Color(0xFFE4E1E6) +val md_theme_dark_inversePrimary = Color(0xFF3950D2) val md_theme_dark_shadow = Color(0xFF000000) -val md_theme_dark_surfaceTint = Color(0xFFD0BCFF) -val md_theme_dark_outlineVariant = Color(0xFF49454F) +val md_theme_dark_surfaceTint = Color(0xFFBBC3FF) +val md_theme_dark_outlineVariant = Color(0xFF46464F) val md_theme_dark_scrim = Color(0xFF000000) -val seed = Color(0xFF6750A4) +val seed = Color(0xFF6177F9) diff --git a/app/src/main/java/com/cstef/meshlink/util/struct/Message.kt b/app/src/main/java/com/cstef/meshlink/util/struct/Message.kt index 4eecd17..8b0a676 100644 --- a/app/src/main/java/com/cstef/meshlink/util/struct/Message.kt +++ b/app/src/main/java/com/cstef/meshlink/util/struct/Message.kt @@ -12,6 +12,7 @@ data class Message( ) : java.io.Serializable { class Type { companion object { + const val BENCHMARK = "benchmark" const val TEXT = "text" const val IMAGE = "image" // const val FILE = "file" diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 6b78462..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 6b78462..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/presentation/src/deck.md b/presentation/src/deck.md index 6a41306..7bc9ecb 100644 --- a/presentation/src/deck.md +++ b/presentation/src/deck.md @@ -333,10 +333,6 @@ $$ --- -# Amélioration du Developer Experience (DX) - ---- - # Évaluation * 3 scénarios